ietf-data-importer 0.3.0 → 0.3.1
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/.github/workflows/check_update.yml +7 -7
- data/.gitignore +4 -0
- data/.rubocop.yml +8 -1
- data/.rubocop_todo.yml +49 -0
- data/CLAUDE.md +73 -0
- data/Gemfile +1 -2
- data/README.adoc +32 -24
- data/exe/ietf-data-importer +1 -1
- data/ietf-data-importer.gemspec +3 -2
- data/lib/ietf/data/importer/cli.rb +14 -23
- data/lib/ietf/data/importer/group.rb +39 -4
- data/lib/ietf/data/importer/group_collection.rb +101 -1
- data/lib/ietf/data/importer/scrapers/base_scraper.rb +18 -9
- data/lib/ietf/data/importer/scrapers/ietf_scraper.rb +137 -213
- data/lib/ietf/data/importer/scrapers/irtf_scraper.rb +142 -291
- data/lib/ietf/data/importer/scrapers.rb +7 -35
- data/lib/ietf/data/importer/version.rb +1 -1
- data/lib/ietf/data/importer.rb +56 -66
- metadata +14 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0ec1ce1c3dff96c474c842a8e47047b251e1cc99586485c69dc38520b6c2c617
|
|
4
|
+
data.tar.gz: c746556a80d5b8575619df38f97d40a9dd6b713e53dbfb8ea5ecfd9b02dbea0e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: acfb7dc40db21492a495848becfd74bfe6cce844fdc3ea47531e16bbdb375ae1450fcfe0ff8bc5b347b1dbc91d7636e0377a877c4f89ec16192bca21f7bf8b15
|
|
7
|
+
data.tar.gz: 81586059d68dbc27f3a6d8a76dbe2cc19c4a64eb062cbcd2e333c44a0c9dbe7922ae1b382a9bf07ef8eda697bb27a1f4523224a0fe01439e7b7b4dfb81bf583a
|
|
@@ -10,7 +10,7 @@ jobs:
|
|
|
10
10
|
name: Check workgroups update
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
12
|
steps:
|
|
13
|
-
- uses: actions/checkout@
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
14
|
|
|
15
15
|
- run: |
|
|
16
16
|
git config user.name github-actions
|
|
@@ -18,19 +18,19 @@ jobs:
|
|
|
18
18
|
|
|
19
19
|
- uses: ruby/setup-ruby@v1
|
|
20
20
|
with:
|
|
21
|
-
ruby-version: '
|
|
21
|
+
ruby-version: '3.0'
|
|
22
22
|
bundler-cache: true
|
|
23
23
|
|
|
24
|
-
- id:
|
|
24
|
+
- id: update
|
|
25
25
|
run: |
|
|
26
|
-
bundle exec
|
|
26
|
+
bundle exec exe/ietf-data-importer fetch lib/ietf/data/importer/groups.yaml
|
|
27
27
|
git diff-index --quiet HEAD -- && has_changes="false" || has_changes="true"
|
|
28
|
-
echo "
|
|
28
|
+
echo "has_changes=${has_changes}" >> "$GITHUB_OUTPUT"
|
|
29
29
|
|
|
30
|
-
- if:
|
|
30
|
+
- if: steps.update.outputs.has_changes == 'true'
|
|
31
31
|
run: gem bump --version patch --tag --push
|
|
32
32
|
|
|
33
|
-
- if:
|
|
33
|
+
- if: steps.update.outputs.has_changes == 'true'
|
|
34
34
|
name: publish to rubygems.org
|
|
35
35
|
env:
|
|
36
36
|
RUBYGEMS_API_KEY: ${{secrets.METANORMA_CI_RUBYGEMS_API_KEY}}
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
# Auto-generated by Cimas: Do not edit it manually!
|
|
2
2
|
# See https://github.com/metanorma/cimas
|
|
3
3
|
inherit_from:
|
|
4
|
+
- .rubocop_todo.yml
|
|
4
5
|
- https://raw.githubusercontent.com/riboseinc/oss-guides/master/ci/rubocop.yml
|
|
5
6
|
|
|
6
7
|
# local repo-specific modifications
|
|
7
8
|
# ...
|
|
8
9
|
|
|
10
|
+
plugins:
|
|
11
|
+
- rubocop-performance
|
|
12
|
+
|
|
9
13
|
AllCops:
|
|
10
|
-
TargetRubyVersion:
|
|
14
|
+
TargetRubyVersion: 3.0
|
|
15
|
+
|
|
16
|
+
Metrics/MethodLength:
|
|
17
|
+
Max: 27
|
data/.rubocop_todo.yml
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# This configuration was generated by
|
|
2
|
+
# `rubocop --auto-gen-config`
|
|
3
|
+
# on 2026-05-12 10:00:04 UTC using RuboCop version 1.86.1.
|
|
4
|
+
# The point is for the user to remove these configuration records
|
|
5
|
+
# one by one as the offenses are removed from the code base.
|
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
|
8
|
+
|
|
9
|
+
# Offense count: 14
|
|
10
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
11
|
+
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
|
|
12
|
+
# URISchemes: http, https
|
|
13
|
+
Layout/LineLength:
|
|
14
|
+
Exclude:
|
|
15
|
+
- 'lib/ietf/data/importer/scrapers/ietf_scraper.rb'
|
|
16
|
+
- 'lib/ietf/data/importer/scrapers/irtf_scraper.rb'
|
|
17
|
+
- 'spec/ietf/data/importer/cli_spec.rb'
|
|
18
|
+
- 'spec/ietf/data/importer/group_collection_spec.rb'
|
|
19
|
+
|
|
20
|
+
# Offense count: 6
|
|
21
|
+
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
|
|
22
|
+
Metrics/AbcSize:
|
|
23
|
+
Exclude:
|
|
24
|
+
- 'lib/ietf/data/importer/scrapers/ietf_scraper.rb'
|
|
25
|
+
- 'lib/ietf/data/importer/scrapers/irtf_scraper.rb'
|
|
26
|
+
|
|
27
|
+
# Offense count: 4
|
|
28
|
+
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
29
|
+
Metrics/CyclomaticComplexity:
|
|
30
|
+
Exclude:
|
|
31
|
+
- 'lib/ietf/data/importer/scrapers/ietf_scraper.rb'
|
|
32
|
+
- 'lib/ietf/data/importer/scrapers/irtf_scraper.rb'
|
|
33
|
+
|
|
34
|
+
# Offense count: 10
|
|
35
|
+
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
36
|
+
Metrics/MethodLength:
|
|
37
|
+
Max: 27
|
|
38
|
+
|
|
39
|
+
# Offense count: 4
|
|
40
|
+
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
41
|
+
Metrics/PerceivedComplexity:
|
|
42
|
+
Exclude:
|
|
43
|
+
- 'lib/ietf/data/importer/scrapers/ietf_scraper.rb'
|
|
44
|
+
- 'lib/ietf/data/importer/scrapers/irtf_scraper.rb'
|
|
45
|
+
|
|
46
|
+
# Offense count: 1
|
|
47
|
+
Security/Open:
|
|
48
|
+
Exclude:
|
|
49
|
+
- 'lib/ietf/data/importer/scrapers/base_scraper.rb'
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
`ietf-data-importer` is a Ruby gem providing offline access to IETF working group and IRTF research group metadata. It ships bundled YAML data and includes web scrapers to refresh it from `datatracker.ietf.org` and `irtf.org`.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
bundle install # install dependencies
|
|
13
|
+
bundle exec rake # run tests (default task = rspec)
|
|
14
|
+
bundle exec rspec # run full test suite
|
|
15
|
+
bundle exec rspec spec/ietf/data/importer_spec.rb:45 # run single test by line
|
|
16
|
+
bundle exec rubocop # lint
|
|
17
|
+
exe/ietf-data-importer fetch output.yaml # scrape & write YAML
|
|
18
|
+
exe/ietf-data-importer fetch output.json --format=json
|
|
19
|
+
exe/ietf-data-importer integrate groups.yaml # embed YAML into gem
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Architecture
|
|
23
|
+
|
|
24
|
+
Namespace: `Ietf::Data::Importer`
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
lib/ietf/data/importer.rb # Entry point — autoload, thin facade delegating to GroupCollection
|
|
28
|
+
lib/ietf/data/importer/
|
|
29
|
+
version.rb # VERSION constant
|
|
30
|
+
group.rb # Group model (Lutaml::Model) with predicate methods
|
|
31
|
+
group_collection.rb # Rich collection model (Enumerable, query methods, merge, from_file, save)
|
|
32
|
+
groups.yaml # Bundled group data (shipped in gem)
|
|
33
|
+
cli.rb # Thor CLI (fetch / integrate commands)
|
|
34
|
+
scrapers.rb # Scrapers.fetch_all / fetch_ietf / fetch_irtf → GroupCollections
|
|
35
|
+
scrapers/
|
|
36
|
+
base_scraper.rb # Abstract: fetch_html, log, build_group, build_collection
|
|
37
|
+
ietf_scraper.rb # Scrapes datatracker.ietf.org → GroupCollection
|
|
38
|
+
irtf_scraper.rb # Scrapes irtf.org → GroupCollection
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Layer separation:**
|
|
42
|
+
- **Models** (`Group`, `GroupCollection`) — all data and query logic. GroupCollection includes Enumerable and supports chainable filters returning new GroupCollections (`collection.active.by_type("wg")`).
|
|
43
|
+
- **Facade** (`Importer` module) — loads bundled `groups.yaml` via `GroupCollection.from_file`, delegates query methods to the collection for backward-compatible API. Uses `autoload` (following sts-ruby pattern).
|
|
44
|
+
- **Scrapers** — each returns a `GroupCollection`. `Scrapers.fetch_all` uses `merge` to combine results. `BaseScraper` provides `build_group`/`build_collection` template methods.
|
|
45
|
+
- **CLI** (`Cli`) — thin Thor wrapper calling scraper and collection methods.
|
|
46
|
+
|
|
47
|
+
**Key design decisions:**
|
|
48
|
+
- Models use `lutaml-model` for serialization — attribute declarations + key_value mapping blocks, same as sts-ruby
|
|
49
|
+
- GroupCollection filter methods (`by_organization`, `by_type`, `by_area`, `active`, `concluded`) return new GroupCollections, enabling chaining (open/closed principle)
|
|
50
|
+
- All facade query methods return GroupCollection; only `groups` returns Array
|
|
51
|
+
- `Group` has predicate methods (`active?`, `ietf?`, `working_group?`) — business logic on the model, not in collection filters
|
|
52
|
+
- `GroupCollection.merge(other)` combines collections immutably
|
|
53
|
+
- `GroupCollection.from_file(path)` / `#save(path, format:)` handle persistence
|
|
54
|
+
- Entry point (`exe/ietf-data-importer`) requires the main importer file, triggering autoload — no direct requires in cli.rb
|
|
55
|
+
- Tests inject data by stubbing `Importer.collection` — no private state access or `class_variable_set`
|
|
56
|
+
- Scrapers are resilient to site layout changes: multiple CSS selectors tried as fallbacks
|
|
57
|
+
- Scraper methods return values rather than mutating parameters
|
|
58
|
+
|
|
59
|
+
## Conventions
|
|
60
|
+
|
|
61
|
+
- Ruby 3.0+ required
|
|
62
|
+
- Frozen string literals everywhere
|
|
63
|
+
- Lutaml::Model for all data models (no raw Hash manipulation in the public API)
|
|
64
|
+
- Test fixtures in `spec/fixtures/`; specs in `spec/ietf/data/importer/` (group_spec, group_collection_spec, cli_spec, importer_spec)
|
|
65
|
+
- Tests stub `Importer.collection` scoped to `:query_tests` context — no global stubs, no private state access
|
|
66
|
+
- Rubocop config inherits from riboseinc/oss-guides (remote URL in `.rubocop.yml`)
|
|
67
|
+
|
|
68
|
+
## Reference Project
|
|
69
|
+
|
|
70
|
+
The `sts-ruby` project at `../sts-ruby/` demonstrates the canonical patterns for Lutaml::Model-based gems in this org:
|
|
71
|
+
- `autoload` for lazy-loading modules/classes
|
|
72
|
+
- Each model is a single class in its own file, inheriting `Lutaml::Model::Serializable`
|
|
73
|
+
- One file per class; no `private` send or `respond_to?` — rely on typed interfaces
|
data/Gemfile
CHANGED
data/README.adoc
CHANGED
|
@@ -7,7 +7,8 @@ image:https://img.shields.io/github/commits-since/metanorma/ietf-data-importer/l
|
|
|
7
7
|
|
|
8
8
|
== Purpose
|
|
9
9
|
|
|
10
|
-
IETF Data Importer is a Ruby gem providing access to information about IETF
|
|
10
|
+
IETF Data Importer is a Ruby gem providing access to information about IETF
|
|
11
|
+
working groups and IRTF research groups.
|
|
11
12
|
|
|
12
13
|
It includes:
|
|
13
14
|
|
|
@@ -20,29 +21,6 @@ This gem exists because the official sources often change their layout or may be
|
|
|
20
21
|
* https://datatracker.ietf.org/group/ (formerly tools.ietf.org/wg)
|
|
21
22
|
* https://irtf.org/groups
|
|
22
23
|
|
|
23
|
-
== Migration from metanorma-ietf-data
|
|
24
|
-
|
|
25
|
-
This gem is the renamed and restructured version of `metanorma-ietf-data`. The namespace and file structure have been changed to match other Metanorma data importer gems.
|
|
26
|
-
|
|
27
|
-
To migrate from metanorma-ietf-data:
|
|
28
|
-
|
|
29
|
-
. Replace the following:
|
|
30
|
-
+
|
|
31
|
-
[source,diff]
|
|
32
|
-
----
|
|
33
|
-
- require "metanorma/ietf/data"
|
|
34
|
-
- groups = Metanorma::Ietf::Data.groups
|
|
35
|
-
+ require "ietf/data/importer"
|
|
36
|
-
+ groups = Ietf::Data::Importer.groups
|
|
37
|
-
----
|
|
38
|
-
|
|
39
|
-
. Update your Gemfile:
|
|
40
|
-
+
|
|
41
|
-
[source,diff]
|
|
42
|
-
----
|
|
43
|
-
- gem 'metanorma-ietf-data'
|
|
44
|
-
+ gem 'ietf-data-importer'
|
|
45
|
-
----
|
|
46
24
|
|
|
47
25
|
== Installation
|
|
48
26
|
|
|
@@ -211,6 +189,36 @@ groups:
|
|
|
211
189
|
----
|
|
212
190
|
====
|
|
213
191
|
|
|
192
|
+
== Migration from metanorma-ietf-data
|
|
193
|
+
|
|
194
|
+
The versions 0.1.0 and 0.2.0 of this gem were published under the name
|
|
195
|
+
`metanorma-ietf-data`.
|
|
196
|
+
|
|
197
|
+
The gem was rewritten and republished as `ietf-data-importer` to better reflect
|
|
198
|
+
its purpose at version 0.3.0. The namespace and file structure have been changed
|
|
199
|
+
to match other Metanorma data importer gems.
|
|
200
|
+
|
|
201
|
+
To migrate from metanorma-ietf-data:
|
|
202
|
+
|
|
203
|
+
. Replace the following:
|
|
204
|
+
+
|
|
205
|
+
[source,diff]
|
|
206
|
+
----
|
|
207
|
+
- require "metanorma/ietf/data"
|
|
208
|
+
- groups = Metanorma::Ietf::Data.groups
|
|
209
|
+
+ require "ietf/data/importer"
|
|
210
|
+
+ groups = Ietf::Data::Importer.groups
|
|
211
|
+
----
|
|
212
|
+
|
|
213
|
+
. Update your Gemfile:
|
|
214
|
+
+
|
|
215
|
+
[source,diff]
|
|
216
|
+
----
|
|
217
|
+
- gem 'metanorma-ietf-data'
|
|
218
|
+
+ gem 'ietf-data-importer'
|
|
219
|
+
----
|
|
220
|
+
|
|
221
|
+
|
|
214
222
|
== Copyright
|
|
215
223
|
|
|
216
224
|
This gem is developed, maintained and funded by https://www.ribose.com[Ribose Inc.]
|
data/exe/ietf-data-importer
CHANGED
data/ietf-data-importer.gemspec
CHANGED
|
@@ -21,9 +21,9 @@ Gem::Specification.new do |spec|
|
|
|
21
21
|
spec.license = "BSD-2-Clause"
|
|
22
22
|
spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
|
|
23
23
|
|
|
24
|
-
spec.add_dependency "lutaml-model", "~> 0.
|
|
24
|
+
spec.add_dependency "lutaml-model", "~> 0.8"
|
|
25
|
+
spec.add_dependency "nokogiri", "~> 1.19"
|
|
25
26
|
spec.add_dependency "thor", "~> 1.0"
|
|
26
|
-
spec.add_dependency "nokogiri", "~> 1.18"
|
|
27
27
|
spec.add_dependency "yaml"
|
|
28
28
|
|
|
29
29
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
@@ -34,4 +34,5 @@ Gem::Specification.new do |spec|
|
|
|
34
34
|
spec.bindir = "exe"
|
|
35
35
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
36
36
|
spec.require_paths = ["lib"]
|
|
37
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
37
38
|
end
|
|
@@ -1,45 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
|
-
require "yaml"
|
|
5
4
|
require "fileutils"
|
|
6
|
-
require_relative "group_collection"
|
|
7
|
-
require_relative "scrapers"
|
|
8
5
|
|
|
9
6
|
module Ietf
|
|
10
7
|
module Data
|
|
11
8
|
module Importer
|
|
12
|
-
# Command-line interface for IETF/IRTF group data
|
|
13
9
|
class Cli < Thor
|
|
14
|
-
desc "fetch OUTPUT_FILE", "Fetch IETF/IRTF groups and save to
|
|
15
|
-
option :format, type: :string, default: "yaml",
|
|
10
|
+
desc "fetch OUTPUT_FILE", "Fetch IETF/IRTF groups and save to file"
|
|
11
|
+
option :format, type: :string, default: "yaml",
|
|
12
|
+
desc: "Output format (yaml or json)"
|
|
16
13
|
def fetch(output_file = nil)
|
|
17
14
|
output_file ||= "ietf_groups.#{options[:format]}"
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
collection
|
|
16
|
+
collection = Scrapers.fetch_all
|
|
17
|
+
collection.save(output_file, format: options[:format].to_sym)
|
|
21
18
|
|
|
22
|
-
#
|
|
23
|
-
format = options[:format].to_sym
|
|
24
|
-
Ietf::Data::Importer::Scrapers.save_to_file(collection, output_file, format)
|
|
19
|
+
puts "Saved #{collection.size} groups to #{output_file}"
|
|
25
20
|
end
|
|
26
21
|
|
|
27
22
|
desc "integrate YAML_FILE", "Integrate YAML file as gem data"
|
|
28
23
|
def integrate(yaml_file)
|
|
29
|
-
|
|
30
|
-
begin
|
|
31
|
-
collection = Ietf::Data::Importer::GroupCollection.from_yaml(File.read(yaml_file))
|
|
32
|
-
rescue => e
|
|
33
|
-
puts "Error reading YAML file: #{e.message}"
|
|
34
|
-
exit 1
|
|
35
|
-
end
|
|
24
|
+
collection = GroupCollection.from_yaml(File.read(yaml_file))
|
|
36
25
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
File.write(target_yaml, File.read(yaml_file))
|
|
26
|
+
target = File.join(__dir__, "groups.yaml")
|
|
27
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
28
|
+
File.write(target, File.read(yaml_file))
|
|
41
29
|
|
|
42
|
-
puts "Integrated #{collection.
|
|
30
|
+
puts "Integrated #{collection.size} groups into gem"
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
puts "Error reading YAML file: #{e.message}"
|
|
33
|
+
exit 1
|
|
43
34
|
end
|
|
44
35
|
end
|
|
45
36
|
end
|
|
@@ -5,14 +5,17 @@ require "lutaml/model"
|
|
|
5
5
|
module Ietf
|
|
6
6
|
module Data
|
|
7
7
|
module Importer
|
|
8
|
-
# Represents a single IETF or IRTF group
|
|
9
8
|
class Group < Lutaml::Model::Serializable
|
|
9
|
+
ORGANIZATIONS = %w[ietf irtf].freeze
|
|
10
|
+
STATUSES = %w[active concluded bof proposed].freeze
|
|
11
|
+
TYPES = %w[wg rg area team program dir ag bof].freeze
|
|
12
|
+
|
|
10
13
|
attribute :abbreviation, :string
|
|
11
14
|
attribute :name, :string
|
|
12
|
-
attribute :organization, :string
|
|
13
|
-
attribute :type, :string
|
|
15
|
+
attribute :organization, :string
|
|
16
|
+
attribute :type, :string
|
|
14
17
|
attribute :area, :string
|
|
15
|
-
attribute :status, :string
|
|
18
|
+
attribute :status, :string
|
|
16
19
|
attribute :description, :string
|
|
17
20
|
attribute :chairs, :string, collection: true
|
|
18
21
|
attribute :mailing_list, :string
|
|
@@ -36,6 +39,38 @@ module Ietf
|
|
|
36
39
|
map "charter_url", to: :charter_url
|
|
37
40
|
map "concluded_date", to: :concluded_date
|
|
38
41
|
end
|
|
42
|
+
|
|
43
|
+
def active?
|
|
44
|
+
status == "active"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def concluded?
|
|
48
|
+
status == "concluded"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def bof?
|
|
52
|
+
status == "bof"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def proposed?
|
|
56
|
+
status == "proposed"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def ietf?
|
|
60
|
+
organization == "ietf"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def irtf?
|
|
64
|
+
organization == "irtf"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def working_group?
|
|
68
|
+
type == "wg"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def research_group?
|
|
72
|
+
type == "rg"
|
|
73
|
+
end
|
|
39
74
|
end
|
|
40
75
|
end
|
|
41
76
|
end
|
|
@@ -6,13 +6,113 @@ require_relative "group"
|
|
|
6
6
|
module Ietf
|
|
7
7
|
module Data
|
|
8
8
|
module Importer
|
|
9
|
-
# Represents a collection of IETF and IRTF groups
|
|
10
9
|
class GroupCollection < Lutaml::Model::Serializable
|
|
10
|
+
include Enumerable
|
|
11
|
+
|
|
11
12
|
attribute :groups, Group, collection: true
|
|
12
13
|
|
|
13
14
|
key_value do
|
|
14
15
|
map "groups", to: :groups
|
|
15
16
|
end
|
|
17
|
+
|
|
18
|
+
def each(&block)
|
|
19
|
+
groups.each(&block)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def size
|
|
23
|
+
groups.size
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def empty?
|
|
27
|
+
groups.empty?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def [](abbreviation)
|
|
31
|
+
find_by_abbreviation(abbreviation)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def find_by_abbreviation(abbreviation)
|
|
35
|
+
groups.find do |g|
|
|
36
|
+
g.abbreviation.downcase == abbreviation.to_s.downcase
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def exists?(abbreviation)
|
|
41
|
+
!find_by_abbreviation(abbreviation).nil?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def by_organization(org)
|
|
45
|
+
self.class.new(groups: groups.select do |g|
|
|
46
|
+
g.organization == org.to_s
|
|
47
|
+
end)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def by_type(type)
|
|
51
|
+
self.class.new(groups: groups.select do |g|
|
|
52
|
+
g.type&.downcase == type.to_s.downcase
|
|
53
|
+
end)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def by_area(area)
|
|
57
|
+
self.class.new(groups: groups.select do |g|
|
|
58
|
+
g.area&.downcase == area.to_s.downcase
|
|
59
|
+
end)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def active
|
|
63
|
+
self.class.new(groups: groups.select(&:active?))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def concluded
|
|
67
|
+
self.class.new(groups: groups.select(&:concluded?))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def working_groups
|
|
71
|
+
by_type("wg")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def research_groups
|
|
75
|
+
by_type("rg")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def ietf_groups
|
|
79
|
+
by_organization("ietf")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def irtf_groups
|
|
83
|
+
by_organization("irtf")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def group_types
|
|
87
|
+
groups.filter_map(&:type).uniq.sort
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def areas
|
|
91
|
+
groups.filter_map(&:area).uniq.sort
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def merge(other)
|
|
95
|
+
self.class.new(groups: groups + other.groups)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.from_file(path, format: :yaml)
|
|
99
|
+
return new(groups: []) unless File.exist?(path)
|
|
100
|
+
|
|
101
|
+
content = File.read(path)
|
|
102
|
+
case format
|
|
103
|
+
when :yaml then from_yaml(content)
|
|
104
|
+
when :json then from_json(content)
|
|
105
|
+
else raise ArgumentError, "Unsupported format: #{format}"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def save(path, format: :yaml)
|
|
110
|
+
case format
|
|
111
|
+
when :yaml then File.write(path, to_yaml)
|
|
112
|
+
when :json then File.write(path, to_json)
|
|
113
|
+
else raise ArgumentError, "Unsupported format: #{format}"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
16
116
|
end
|
|
17
117
|
end
|
|
18
118
|
end
|
|
@@ -2,30 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
require "nokogiri"
|
|
4
4
|
require "open-uri"
|
|
5
|
+
require_relative "../group"
|
|
6
|
+
require_relative "../group_collection"
|
|
5
7
|
|
|
6
8
|
module Ietf
|
|
7
9
|
module Data
|
|
8
10
|
module Importer
|
|
9
11
|
module Scrapers
|
|
10
|
-
# Base class for web scrapers
|
|
11
12
|
class BaseScraper
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
def fetch
|
|
14
|
+
raise NotImplementedError, "#{self.class}#fetch must be implemented"
|
|
15
|
+
end
|
|
16
|
+
|
|
15
17
|
def fetch_html(url)
|
|
16
18
|
Nokogiri::HTML(URI.open(url))
|
|
17
|
-
rescue => e
|
|
18
|
-
|
|
19
|
+
rescue StandardError => e
|
|
20
|
+
log "Error fetching URL #{url}: #{e.message}"
|
|
19
21
|
nil
|
|
20
22
|
end
|
|
21
23
|
|
|
22
|
-
# Log a message with indentation
|
|
23
|
-
# @param message [String] The message to log
|
|
24
|
-
# @param level [Integer] The indentation level (default: 0)
|
|
25
24
|
def log(message, level = 0)
|
|
26
25
|
indent = " " * level
|
|
27
26
|
puts "#{indent}#{message}"
|
|
28
27
|
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def build_group(attributes)
|
|
32
|
+
Group.new(attributes)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def build_collection(groups)
|
|
36
|
+
GroupCollection.new(groups: groups)
|
|
37
|
+
end
|
|
29
38
|
end
|
|
30
39
|
end
|
|
31
40
|
end
|