calendarium-romanum-remote 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1922aae05ab92482cef8245ff0af153704256dcf
4
+ data.tar.gz: 37b09a459b50be3e1e8a85d5df6a55c903bb87dc
5
+ SHA512:
6
+ metadata.gz: a9d35cac14c7720322963a0470e7025d25a3ca834ddfe7d822459ff89d0e210d4f3125c0c91013215e0faab7d466f27ecfcf01f41add018aa4678a900a82f8b8
7
+ data.tar.gz: f0306395509292979378465024b9b38bc6378acd38c195de2aa89790edcfb92df0d02d25ae49d710c27c9a28c747d2ff8344680473aae43e5c8e51ca17a2d7a9
@@ -0,0 +1,2 @@
1
+ require 'calendarium-romanum'
2
+ require_relative 'calendarium-romanum/remote'
@@ -0,0 +1,17 @@
1
+ module CalendariumRomanum
2
+ module Remote; end
3
+ end
4
+
5
+ require 'net/http'
6
+ require 'json'
7
+
8
+ %w(
9
+ version
10
+ calendar
11
+ drivers
12
+ drivers/net_http_driver
13
+ errors
14
+ v0/deserializer
15
+ ).each do |path|
16
+ require_relative File.join('remote', path)
17
+ end
@@ -0,0 +1,75 @@
1
+ module CalendariumRomanum
2
+ module Remote
3
+ # Mostly API-compatible with CalendariumRomanum::Calendar
4
+ # (only constructor differs).
5
+ # Instead of computing calendar data, obtains them
6
+ # from a remote calendar API
7
+ # https://github.com/igneus/church-calendar-api
8
+ class Calendar
9
+ extend Forwardable
10
+
11
+ def initialize(year, calendar_uri, api_version: :v0, driver: :net_http)
12
+ @year = year
13
+ @calendar_uri = calendar_uri
14
+ @driver =
15
+ if driver.is_a? Symbol
16
+ # built-in driver specified by a Symbol
17
+ Drivers.get(api_version, driver)
18
+ else
19
+ # driver instance
20
+ driver
21
+ end
22
+ @deserializer = V0::Deserializer.new
23
+
24
+ # only for most fundamental computations made locally
25
+ @temporale = Temporale.new(year)
26
+ # only for API compatibility
27
+ @sanctorale = nil
28
+ end
29
+
30
+ attr_reader :year
31
+ attr_reader :temporale
32
+ attr_reader :sanctorale
33
+ attr_reader :calendar_uri
34
+
35
+ def_delegators :@temporale, :range_check, :season
36
+
37
+ def day(*args)
38
+ # TODO code copied from CalendariumRomanum::Calendar -
39
+ # extract to a separate method
40
+ if args.size == 2
41
+ date = Date.new(@year, *args)
42
+ unless @temporale.date_range.include? date
43
+ date = Date.new(@year + 1, *args)
44
+ end
45
+ else
46
+ date = CalendariumRomanum::Calendar.mk_date *args
47
+ #range_check date
48
+ end
49
+
50
+ serialized = @driver.get date, @calendar_uri
51
+ @deserializer.call serialized
52
+ end
53
+
54
+ def lectionary
55
+ year_spec['lectionary'].to_sym
56
+ end
57
+
58
+ def ferial_lectionary
59
+ year_spec['ferial_lectionary'].to_i
60
+ end
61
+
62
+ def ==(obj)
63
+ self.class == obj.class &&
64
+ self.year == obj.year &&
65
+ self.calendar_uri == obj.calendar_uri
66
+ end
67
+
68
+ private
69
+
70
+ def year_spec
71
+ @year_spec ||= JSON.parse(@driver.year(@year, @calendar_uri))
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,17 @@
1
+ module CalendariumRomanum
2
+ module Remote
3
+ module Drivers
4
+ def self.get(api_version, driver_id)
5
+ if api_version == :v0
6
+ if driver_id == :net_http
7
+ return NetHttpDriver.new
8
+ else
9
+ raise ArgumentError.new("Unsupported driver #{driver_id}")
10
+ end
11
+ else
12
+ raise ArgumentError.new("Unsupported API version #{api_version.inspect}")
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,52 @@
1
+ module CalendariumRomanum
2
+ module Remote
3
+ module Drivers
4
+ # Communicates with the remote API using Ruby standard library
5
+ class NetHttpDriver
6
+ def get(date, calendar_uri)
7
+ uri_str =
8
+ calendar_uri +
9
+ (calendar_uri.end_with?('/') ? '' : '/') +
10
+ "#{date.year}/#{date.month}/#{date.day}"
11
+ uri = URI(uri_str)
12
+
13
+ get_request uri
14
+ end
15
+
16
+ def year(year, calendar_uri)
17
+ uri_str =
18
+ calendar_uri +
19
+ (calendar_uri.end_with?('/') ? '' : '/') +
20
+ year.to_s
21
+ uri = URI(uri_str)
22
+
23
+ get_request uri
24
+ end
25
+
26
+ private
27
+
28
+ def get_request(uri)
29
+ begin
30
+ response = Net::HTTP.get_response uri
31
+ rescue SocketError, Errno::ECONNREFUSED => err
32
+ raise ServerNotFoundError.new err.message
33
+ rescue Timeout::Error, Errno::EINVAL,
34
+ Errno::ECONNRESET, EOFError,
35
+ Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
36
+ Net::ProtocolError => err
37
+ raise TransportError.new err.message
38
+ end
39
+
40
+ if response.code == '200'
41
+ return response.body
42
+ elsif response.code == '400'
43
+ json = JSON.parse(response.body)
44
+ raise BadRequestError.new(json['error'])
45
+ else
46
+ raise Error.new("Unexpected status #{response.code.inspect}")
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,15 @@
1
+ module CalendariumRomanum
2
+ module Remote
3
+ # parent of the gem's error classes,
4
+ # never itself instantiated
5
+ class Error < ::RuntimeError; end
6
+
7
+ # server not found
8
+ class ServerNotFoundError < Error; end
9
+
10
+ # server refuses submitted input
11
+ class BadRequestError < Error; end
12
+
13
+ class TransportError < Error; end
14
+ end
15
+ end
@@ -0,0 +1,28 @@
1
+ module CalendariumRomanum
2
+ module Remote
3
+ module V0
4
+ class Deserializer
5
+ def call(day_str)
6
+ parsed = JSON.parse day_str
7
+
8
+ season_sym = parsed['season'].to_sym
9
+
10
+ CalendariumRomanum::Day.new(
11
+ date: Date.parse(parsed['date']),
12
+ season: CalendariumRomanum::Seasons.all.find {|s| s.symbol == season_sym},
13
+ season_week: parsed['season_week'],
14
+ celebrations: parsed['celebrations'].collect do |c|
15
+ colour_sym = c['colour'].to_sym
16
+
17
+ CalendariumRomanum::Celebration.new(
18
+ c['title'],
19
+ CalendariumRomanum::Ranks[c['rank_num']],
20
+ CalendariumRomanum::Colours.all.find {|c| c.symbol == colour_sym }
21
+ )
22
+ end
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,6 @@
1
+ module CalendariumRomanum
2
+ module Remote
3
+ VERSION = '0.1.0'
4
+ RELEASE_DATE = Date.new(2017, 9, 2)
5
+ end
6
+ end
@@ -0,0 +1,104 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe CalendariumRomanum::Remote::Calendar do
4
+ CR = CalendariumRomanum
5
+ CRR = CalendariumRomanum::Remote
6
+
7
+ let(:year) { 2016 }
8
+ let(:calendar) { described_class.new year, REMOTE_CALENDAR_URI }
9
+
10
+ describe '#year' do
11
+ it 'returns the year passed to the constructor' do
12
+ y = 2200
13
+ cal = described_class.new y, REMOTE_CALENDAR_URI
14
+ expect(cal.year).to eq y
15
+ end
16
+ end
17
+
18
+ describe '#lectionary' do
19
+ it { expect(calendar.lectionary).to be :A }
20
+ end
21
+
22
+ describe '#ferial_lectionary' do
23
+ it { expect(calendar.ferial_lectionary).to be 1 }
24
+ end
25
+
26
+ describe '#==' do
27
+ describe 'same year and URI' do
28
+ let(:c) { described_class.new year, REMOTE_CALENDAR_URI }
29
+
30
+ it 'is same' do
31
+ expect(c == calendar).to be true
32
+ end
33
+ end
34
+
35
+ describe 'year differs' do
36
+ let(:c) { described_class.new year + 1, REMOTE_CALENDAR_URI }
37
+
38
+ it 'is different' do
39
+ expect(c == calendar).to be false
40
+ end
41
+ end
42
+
43
+ describe 'URI differs' do
44
+ let(:c) { described_class.new year, 'http://other.uri' }
45
+
46
+ it 'is different' do
47
+ expect(c == calendar).to be false
48
+ end
49
+ end
50
+ end
51
+
52
+ describe 'API-compatibility with Calendar' do
53
+ cls = CR::Calendar
54
+
55
+ describe 'instance methods' do
56
+ let(:year) { 2000 }
57
+
58
+ let(:origin) { CR::Calendar.new(year) }
59
+ let(:mirror) { CRR::Calendar.new(year, REMOTE_CALENDAR_URI) }
60
+
61
+ cls.public_instance_methods.each do |method|
62
+ describe method do
63
+ it 'is defined by origin' do
64
+ expect(origin).to respond_to method
65
+ end
66
+
67
+ it 'is defined by mirror' do
68
+ expect(mirror).to respond_to method
69
+ end
70
+
71
+ it 'definitions match' do
72
+ morig = origin.method method
73
+ mmirr = mirror.method method
74
+
75
+ expect(mmirr.arity).to eq morig.arity
76
+ expect(mmirr.parameters).to eq morig.parameters
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ describe 'returns the same data as Calendar' do
84
+ let(:year) { 2016 }
85
+
86
+ # classical calendar
87
+ let(:sanctorale) { CR::Data::GENERAL_ROMAN_ENGLISH.load }
88
+ let(:calendar) { CR::Calendar.new(year, sanctorale) }
89
+
90
+ # remote calendar with the same settings
91
+ let(:uri) { REMOTE_CALENDAR_URI }
92
+ let(:remote) { described_class.new(year, uri) }
93
+
94
+ days = (CR::Temporale::Dates.first_advent_sunday(2016) ... CR::Temporale::Dates.first_advent_sunday(2017))
95
+ days.each do |date|
96
+ it date do
97
+ c = calendar.day date
98
+ r = remote.day date
99
+
100
+ expect(r).to eq c
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+
3
+ describe CalendariumRomanum::Remote::Drivers do
4
+ CRRem = CalendariumRomanum::Remote
5
+
6
+ let(:base_uri) { REMOTE_CALENDAR_URI }
7
+ let(:date) { Date.new 2000, 1, 1 }
8
+
9
+ shared_examples 'any driver' do
10
+ describe 'success' do
11
+ it 'returns a string' do
12
+ r = driver.get date, base_uri
13
+ expect(r).to be_a String
14
+ expect(r).not_to be_empty
15
+ end
16
+
17
+ it 'returns a valid JSON' do
18
+ r = driver.get date, base_uri
19
+ expect do
20
+ JSON.parse(r)
21
+ end.not_to raise_exception
22
+ end
23
+ end
24
+
25
+ describe 'server not found' do
26
+ let(:base_uri) { 'http://unknown.server.xyz' }
27
+
28
+ it 'fails' do
29
+ expect do
30
+ driver.get date, base_uri
31
+ end.to raise_exception CRRem::ServerNotFoundError
32
+ end
33
+ end
34
+
35
+ describe 'connection refused' do
36
+ let(:base_uri) { 'http://localhost:33333' }
37
+
38
+ it 'fails' do
39
+ expect do
40
+ driver.get date, base_uri
41
+ end.to raise_exception CRRem::ServerNotFoundError
42
+ end
43
+ end
44
+
45
+ describe 'calendar not found' do
46
+ let(:base_uri) { REMOTE_CALENDAR_URI.sub(/(?<=calendars\/).*$/, 'unknown-calendar') }
47
+
48
+ it 'fails' do
49
+ expect do
50
+ driver.get date, base_uri
51
+ end.to raise_exception CRRem::BadRequestError
52
+ end
53
+ end
54
+
55
+ describe 'invalid date' do
56
+ # this won't ever happen, but the driver must be prepared
57
+ # to handle bad request scenarios
58
+ let(:date) { double(year: 2000, month: 13, day: 40) }
59
+
60
+ it 'fails' do
61
+ expect do
62
+ driver.get date, base_uri
63
+ end.to raise_exception CRRem::BadRequestError
64
+ end
65
+ end
66
+ end
67
+
68
+ describe CalendariumRomanum::Remote::Drivers::NetHttpDriver do
69
+ let(:driver) { described_class.new }
70
+
71
+ it_behaves_like 'any driver'
72
+
73
+ # implementation-specific error handling
74
+ describe 'network error' do
75
+ [
76
+ Timeout::Error,
77
+ Errno::EINVAL,
78
+ Errno::ECONNRESET,
79
+ EOFError,
80
+ Net::HTTPBadResponse,
81
+ Net::HTTPHeaderSyntaxError,
82
+ Net::ProtocolError
83
+ ].each do |etype|
84
+ it 'fails' do
85
+ allow(Net::HTTP).to receive(:get_response).and_raise(etype)
86
+
87
+ expect do
88
+ driver.get date, base_uri
89
+ end.to raise_exception CRRem::TransportError
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+
3
+ class MarkdownDocument
4
+ def initialize(str)
5
+ @str = str
6
+ end
7
+
8
+ def each_ruby_example
9
+ example = nil
10
+ line = nil
11
+ @str.each_line.with_index(1) do |l, i|
12
+ if example.nil?
13
+ if example_beginning?(l)
14
+ example = ''
15
+ line = i + 1
16
+ end
17
+ elsif example_end?(l)
18
+ yield example, line
19
+ example = nil
20
+ else
21
+ example += l
22
+ end
23
+ end
24
+ end
25
+
26
+ protected
27
+
28
+ def example_beginning?(line)
29
+ line =~ /^```ruby/
30
+ end
31
+
32
+ def example_end?(line)
33
+ line =~ /```/
34
+ end
35
+ end
36
+
37
+ %w(README.md).each do |path|
38
+ describe path do
39
+ readme_path = File.expand_path('../../' + path, __FILE__)
40
+ readme = File.read readme_path
41
+ doc = MarkdownDocument.new readme
42
+
43
+ doc.each_ruby_example do |code, line|
44
+ describe "example L#{line}" do
45
+ it 'executes without failure' do
46
+ cls = Class.new
47
+ cls.class_eval(code, readme_path, line)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,105 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
4
+ # this file to always be loaded, without a need to explicitly require it in any
5
+ # files.
6
+ #
7
+ # Given that it is always loaded, you are encouraged to keep this file as
8
+ # light-weight as possible. Requiring heavyweight dependencies from this file
9
+ # will add to the boot time of your test suite on EVERY test run, even for an
10
+ # individual file that may not need all of that loaded. Instead, consider making
11
+ # a separate helper file that requires the additional dependencies and performs
12
+ # the additional setup, and require it from the spec files that actually need
13
+ # it.
14
+ #
15
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
16
+ RSpec.configure do |config|
17
+ # rspec-expectations config goes here. You can use an alternate
18
+ # assertion/expectation library such as wrong or the stdlib/minitest
19
+ # assertions if you prefer.
20
+ config.expect_with :rspec do |expectations|
21
+ # This option will default to `true` in RSpec 4. It makes the `description`
22
+ # and `failure_message` of custom matchers include text for helper methods
23
+ # defined using `chain`, e.g.:
24
+ # be_bigger_than(2).and_smaller_than(4).description
25
+ # # => "be bigger than 2 and smaller than 4"
26
+ # ...rather than:
27
+ # # => "be bigger than 2"
28
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
29
+ end
30
+
31
+ # rspec-mocks config goes here. You can use an alternate test double
32
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
33
+ config.mock_with :rspec do |mocks|
34
+ # Prevents you from mocking or stubbing a method that does not exist on
35
+ # a real object. This is generally recommended, and will default to
36
+ # `true` in RSpec 4.
37
+ mocks.verify_partial_doubles = true
38
+ end
39
+
40
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
41
+ # have no way to turn it off -- the option exists only for backwards
42
+ # compatibility in RSpec 3). It causes shared context metadata to be
43
+ # inherited by the metadata hash of host groups and examples, rather than
44
+ # triggering implicit auto-inclusion in groups with matching metadata.
45
+ config.shared_context_metadata_behavior = :apply_to_host_groups
46
+
47
+ # The settings below are suggested to provide a good initial experience
48
+ # with RSpec, but feel free to customize to your heart's content.
49
+ =begin
50
+ # This allows you to limit a spec run to individual examples or groups
51
+ # you care about by tagging them with `:focus` metadata. When nothing
52
+ # is tagged with `:focus`, all examples get run. RSpec also provides
53
+ # aliases for `it`, `describe`, and `context` that include `:focus`
54
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
55
+ config.filter_run_when_matching :focus
56
+
57
+ # Allows RSpec to persist some state between runs in order to support
58
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
59
+ # you configure your source control system to ignore this file.
60
+ config.example_status_persistence_file_path = "spec/examples.txt"
61
+
62
+ # Limits the available syntax to the non-monkey patched syntax that is
63
+ # recommended. For more details, see:
64
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
65
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
66
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
67
+ config.disable_monkey_patching!
68
+
69
+ # This setting enables warnings. It's recommended, but in some cases may
70
+ # be too noisy due to issues in dependencies.
71
+ config.warnings = true
72
+
73
+ # Many RSpec users commonly either run the entire suite or an individual
74
+ # file, and it's useful to allow more verbose output when running an
75
+ # individual spec file.
76
+ if config.files_to_run.one?
77
+ # Use the documentation formatter for detailed output,
78
+ # unless a formatter has already been configured
79
+ # (e.g. via a command-line flag).
80
+ config.default_formatter = "doc"
81
+ end
82
+
83
+ # Print the 10 slowest examples and example groups at the
84
+ # end of the spec run, to help surface which specs are running
85
+ # particularly slow.
86
+ config.profile_examples = 10
87
+
88
+ # Run specs in random order to surface order dependencies. If you find an
89
+ # order dependency and want to debug it, you can fix the order by providing
90
+ # the seed, which is printed after each run.
91
+ # --seed 1234
92
+ config.order = :random
93
+
94
+ # Seed global randomization in this process using the `--seed` CLI option.
95
+ # Setting this allows you to use `--seed` to deterministically reproduce
96
+ # test failures related to randomization by passing the same `--seed` value
97
+ # as the one that triggered the failure.
98
+ Kernel.srand config.seed
99
+ =end
100
+ end
101
+
102
+ require_relative '../lib/calendarium-romanum-remote'
103
+
104
+ REMOTE_CALENDAR_URI =
105
+ ENV['REMOTE_CALENDAR'] || 'http://calapi.inadiutorium.cz/api/v0/en/calendars/general-en/'
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: calendarium-romanum-remote
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jakub Pavlík
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-09-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: calendarium-romanum
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.4.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.4.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '12.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '12.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.6'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.6'
55
+ description: obtains calendar data from an API
56
+ email: jkb.pavlik@gmail.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - lib/calendarium-romanum-remote.rb
62
+ - lib/calendarium-romanum/remote.rb
63
+ - lib/calendarium-romanum/remote/calendar.rb
64
+ - lib/calendarium-romanum/remote/drivers.rb
65
+ - lib/calendarium-romanum/remote/drivers/net_http_driver.rb
66
+ - lib/calendarium-romanum/remote/errors.rb
67
+ - lib/calendarium-romanum/remote/v0/deserializer.rb
68
+ - lib/calendarium-romanum/remote/version.rb
69
+ - spec/calendar_spec.rb
70
+ - spec/drivers_spec.rb
71
+ - spec/readme_spec.rb
72
+ - spec/spec_helper.rb
73
+ homepage: http://github.com/igneus/calendarium-romanum-remote
74
+ licenses:
75
+ - LGPL-3.0
76
+ - MIT
77
+ metadata: {}
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubyforge_project:
94
+ rubygems_version: 2.5.1
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: remote calendar extension for calendarium-romanum
98
+ test_files: []