aeternitas 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 +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +8 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +289 -0
- data/Rakefile +6 -0
- data/aeternitas.gemspec +44 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/aeternitas.rb +80 -0
- data/lib/aeternitas/errors.rb +48 -0
- data/lib/aeternitas/guard.rb +199 -0
- data/lib/aeternitas/metrics.rb +162 -0
- data/lib/aeternitas/metrics/counter.rb +18 -0
- data/lib/aeternitas/metrics/ratio.rb +67 -0
- data/lib/aeternitas/metrics/ten_minutes_resolution.rb +40 -0
- data/lib/aeternitas/metrics/values.rb +18 -0
- data/lib/aeternitas/pollable.rb +197 -0
- data/lib/aeternitas/pollable/configuration.rb +64 -0
- data/lib/aeternitas/pollable/dsl.rb +124 -0
- data/lib/aeternitas/pollable_meta_data.rb +73 -0
- data/lib/aeternitas/polling_frequency.rb +24 -0
- data/lib/aeternitas/sidekiq.rb +5 -0
- data/lib/aeternitas/sidekiq/middleware.rb +31 -0
- data/lib/aeternitas/sidekiq/poll_job.rb +30 -0
- data/lib/aeternitas/source.rb +62 -0
- data/lib/aeternitas/storage_adapter.rb +46 -0
- data/lib/aeternitas/storage_adapter/file.rb +73 -0
- data/lib/aeternitas/version.rb +3 -0
- data/lib/generators/aeternitas/install_generator.rb +35 -0
- data/lib/generators/aeternitas/templates/add_aeternitas.rb.erb +25 -0
- data/lib/generators/aeternitas/templates/initializer.rb +10 -0
- data/logo.png +0 -0
- data/logo.svg +198 -0
- metadata +289 -0
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'aasm'
|
2
|
+
|
3
|
+
module Aeternitas
|
4
|
+
# Stores the meta data of all pollables
|
5
|
+
# Every pollable needs to have exactly one meta data object
|
6
|
+
class PollableMetaData < ActiveRecord::Base
|
7
|
+
self.table_name = 'aeternitas_pollable_meta_data'
|
8
|
+
|
9
|
+
include AASM
|
10
|
+
######
|
11
|
+
# create_table aeternitas_pollable_meta_data do |t|
|
12
|
+
# t.string :pollable_type, null: false
|
13
|
+
# t.integer :pollable_id, null: false
|
14
|
+
# t.string :pollable_class, null: false
|
15
|
+
# t.datetime :next_polling, null: false, default: "1970-01-01 00:00:00+002"
|
16
|
+
# t.datetime :last_polling
|
17
|
+
# t.string :state
|
18
|
+
# t.text :deactivation_reason
|
19
|
+
# t.datetime :deactivated_at
|
20
|
+
# end
|
21
|
+
# create_index :aeternitas_pollable_meta_data, [:pollable_id, :pollable_type], name: 'aeternitas_pollable_unique', unique: true
|
22
|
+
# create_index :aeternitas_pollable_meta_data, [:next_polling, :state], name: 'aeternitas_pollable_enqueueing'
|
23
|
+
# create_index :aeternitas_pollable_meta_data, [:pollable_class], name: 'aeternitas_pollable_class'
|
24
|
+
######
|
25
|
+
|
26
|
+
belongs_to :pollable, polymorphic: true
|
27
|
+
|
28
|
+
validates :pollable_type, presence: true, uniqueness: { scope: :pollable_id }
|
29
|
+
validates :pollable_id, presence: true, uniqueness: { scope: :pollable_type }
|
30
|
+
validates :pollable_class, presence: true
|
31
|
+
validates :next_polling, presence: true
|
32
|
+
|
33
|
+
aasm column: :state do
|
34
|
+
state :waiting, initial: true
|
35
|
+
state :enqueued
|
36
|
+
state :active
|
37
|
+
state :deactivated
|
38
|
+
state :errored
|
39
|
+
|
40
|
+
event :enqueue do
|
41
|
+
transitions from: %i[waiting deactivated errored], to: :enqueued
|
42
|
+
end
|
43
|
+
|
44
|
+
event :poll do
|
45
|
+
transitions from: %i[waiting enqueued errored], to: :active
|
46
|
+
end
|
47
|
+
|
48
|
+
event :has_errored do
|
49
|
+
transitions from: :active, to: :errored
|
50
|
+
end
|
51
|
+
|
52
|
+
event :wait do
|
53
|
+
transitions from: :active, to: :waiting
|
54
|
+
end
|
55
|
+
|
56
|
+
event :deactivate do
|
57
|
+
transitions to: :deactivated
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
scope(:due, ->() { waiting.where('next_polling < ?', Time.now) })
|
62
|
+
|
63
|
+
# Disables polling of this instance
|
64
|
+
#
|
65
|
+
# @param [String] reason Reason for the deactivation. (E.g. an error message)
|
66
|
+
def disable_polling(reason = nil)
|
67
|
+
self.deactivate
|
68
|
+
self.deactivation_reason = reason.to_s
|
69
|
+
self.deactivated_at = Time.now
|
70
|
+
self.save!
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Aeternitas
|
2
|
+
# Stores default polling frequency calculation methods.
|
3
|
+
module PollingFrequency
|
4
|
+
HOURLY = ->(context) { Time.now + 1.hour }
|
5
|
+
DAILY = ->(context) { Time.now + 1.day }
|
6
|
+
WEEKLY = ->(context) { Time.now + 1.week }
|
7
|
+
MONTHLY = ->(context) { Time.now + 1.month }
|
8
|
+
|
9
|
+
# Retrieves the build-in polling frequency methods by name.
|
10
|
+
#
|
11
|
+
# @param [Symbol] name the frequency method
|
12
|
+
# @return [Lambda] Polling frequency method
|
13
|
+
# @raise [ArgumentError] if the preset does not exist
|
14
|
+
def self.by_name(name)
|
15
|
+
case name
|
16
|
+
when :hourly then HOURLY
|
17
|
+
when :daily then DAILY
|
18
|
+
when :weekly then WEEKLY
|
19
|
+
when :monthly then MONTHLY
|
20
|
+
else raise(ArgumentError, "Unknown polling frequency: #{name}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Aeternitas
|
2
|
+
module Sidekiq
|
3
|
+
# Aeternitas Sidekiq Middleware
|
4
|
+
class Middleware
|
5
|
+
|
6
|
+
def call(worker, msg, queue)
|
7
|
+
yield
|
8
|
+
rescue Aeternitas::Guard::GuardIsLocked => e
|
9
|
+
raise e unless worker.is_a? Aeternitas::Sidekiq::PollJob
|
10
|
+
|
11
|
+
# try deleting the sidekiq unique key so the job can be reenqueued
|
12
|
+
Aeternitas.redis.del(
|
13
|
+
SidekiqUniqueJobs::UniqueArgs.digest(msg)
|
14
|
+
)
|
15
|
+
|
16
|
+
# reenqueue the job
|
17
|
+
worker.class.client_push msg
|
18
|
+
|
19
|
+
# update the pollables state
|
20
|
+
meta_data = Aeternitas::PollableMetaData.find_by(id: msg['args'].first)
|
21
|
+
meta_data.enqueue!
|
22
|
+
|
23
|
+
if meta_data.pollable.pollable_configuration.sleep_on_guard_locked
|
24
|
+
# put the worker to rest for errors timeout
|
25
|
+
sleep_duration = (e.timeout - Time.now).to_i
|
26
|
+
sleep(sleep_duration + 1.0) if sleep_duration > 0
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
module Aeternitas
|
3
|
+
module Sidekiq
|
4
|
+
# Sidekiq Worker that is responsible for executing the polling.
|
5
|
+
class PollJob
|
6
|
+
include ::Sidekiq::Worker
|
7
|
+
|
8
|
+
sidekiq_options unique: :until_executed,
|
9
|
+
unique_args: [:pollable_meta_data_id],
|
10
|
+
unique_job_expiration: 1.month.to_i,
|
11
|
+
queue: :polling,
|
12
|
+
retry: 4
|
13
|
+
|
14
|
+
sidekiq_retry_in do |count|
|
15
|
+
[60, 3600, 86400, 604800][count]
|
16
|
+
end
|
17
|
+
|
18
|
+
sidekiq_retries_exhausted do |msg|
|
19
|
+
meta_data = Aeternitas::PollableMetaData.find_by!(id: msg['args'].first)
|
20
|
+
meta_data.disable_polling(msg['error_message'])
|
21
|
+
end
|
22
|
+
|
23
|
+
def perform(pollable_meta_data_id)
|
24
|
+
meta_data = Aeternitas::PollableMetaData.find_by(id: pollable_meta_data_id)
|
25
|
+
pollable = meta_data.pollable
|
26
|
+
pollable.execute_poll
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Aeternitas
|
2
|
+
# Sources can store polling results in a write once - read many fashion.
|
3
|
+
# Each source, by default, hereby stores it's raw_content in a compressed file on disk if it has not been saved yet
|
4
|
+
# (determined by the raw_contents MD5 Hash)
|
5
|
+
# The 'aeternitas_sources' table holds all source metadata in a quite space-efficient way. For instance the compressed
|
6
|
+
# file's location is determined from the sources fingerprint which at the same time is it's database ID.
|
7
|
+
class Source < ActiveRecord::Base
|
8
|
+
######
|
9
|
+
# create_table :aeternitas_sources, id: :string, primary_key: :fingerprint do |t|
|
10
|
+
# t.string :pollable_type, null: false
|
11
|
+
# t.integer :pollable_id, null: false
|
12
|
+
# t.datetime :created_at
|
13
|
+
# end
|
14
|
+
# add_index :aeternitas_sources, [:pollable_id, :pollable_type], name: 'aeternitas_pollable_source'
|
15
|
+
######
|
16
|
+
self.table_name = 'aeternitas_sources'
|
17
|
+
|
18
|
+
attr_writer :raw_content
|
19
|
+
|
20
|
+
belongs_to :pollable, polymorphic: true
|
21
|
+
|
22
|
+
after_initialize :ensure_fingerprint
|
23
|
+
|
24
|
+
validates :raw_content, presence: true, on: :create
|
25
|
+
validates :fingerprint, presence: true, uniqueness: true
|
26
|
+
|
27
|
+
# Ensure that the file was created before the record is saved
|
28
|
+
before_create :create_file
|
29
|
+
|
30
|
+
# Make sure to delete the file if the transaction that includes the creation is aborted
|
31
|
+
after_rollback :delete_file, on: :create
|
32
|
+
|
33
|
+
# Make sure to delete the file only if the record was safely destroyed
|
34
|
+
after_commit :delete_file, on: :destroy
|
35
|
+
|
36
|
+
# Generates the entries fingerprint.
|
37
|
+
# @return [String] the entries fingerprint.
|
38
|
+
def generate_fingerprint
|
39
|
+
Digest::MD5.hexdigest(@raw_content.to_s)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Get the sources raw content.
|
43
|
+
# @return [String] the sources raw content
|
44
|
+
def raw_content
|
45
|
+
@raw_content ||= Aeternitas.config.get_storage_adapter.retrieve(self.fingerprint)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def create_file
|
51
|
+
Aeternitas.config.get_storage_adapter.store(self.fingerprint, raw_content)
|
52
|
+
end
|
53
|
+
|
54
|
+
def delete_file
|
55
|
+
Aeternitas.config.get_storage_adapter.delete(self.fingerprint)
|
56
|
+
end
|
57
|
+
|
58
|
+
def ensure_fingerprint
|
59
|
+
self.fingerprint ||= generate_fingerprint
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "aeternitas/storage_adapter/file"
|
2
|
+
module Aeternitas
|
3
|
+
# Storage Adapters take care of handling source files.
|
4
|
+
# @abstract Create a subclass and override {#store}, #{retrieve} and #{#delete} to create a new storage adapter
|
5
|
+
class StorageAdapter
|
6
|
+
|
7
|
+
# Create a new storage adapter
|
8
|
+
# @param [Hash] config the adapters configuration
|
9
|
+
def initialize(config)
|
10
|
+
@config = config
|
11
|
+
end
|
12
|
+
|
13
|
+
# Store a new entry with the given id and raw content
|
14
|
+
# @abstract
|
15
|
+
# @param [String] id the entries fingerprint
|
16
|
+
# @param [Object] raw_content the raw content object
|
17
|
+
def store(id, raw_content)
|
18
|
+
raise NotImplementedError, "#{self.class.name} does not implement #store, required by Aeternitas::StorageAdapter"
|
19
|
+
end
|
20
|
+
|
21
|
+
# Retrieves the content of the entry with the given fingerprint
|
22
|
+
# @abstract
|
23
|
+
# @param [String] id the entries fingerprint
|
24
|
+
# @return [String] the entries content
|
25
|
+
def retrieve(id)
|
26
|
+
raise NotImplementedError, "#{self.class.name} does not implement #retrive, required by Aeternitas::StorageAdapter"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Delete the entry with the given fingerprint
|
30
|
+
# @abstract
|
31
|
+
# @param [String] id the entries fingerprint
|
32
|
+
# @return [Boolean] Operation state
|
33
|
+
def delete(id)
|
34
|
+
raise NotImplementedError, "#{self.class.name} does not implement #delete, required by Aeternitas::StorageAdapter"
|
35
|
+
end
|
36
|
+
|
37
|
+
# Checks whether the entry with the given fingerprint exists.
|
38
|
+
# @abstract
|
39
|
+
# @param [String] id the entries id
|
40
|
+
# @return [Boolean] if the entry exists
|
41
|
+
def exist?(id)
|
42
|
+
raise NotImplementedError, "#{self.class.name} does not implement #exist?, required by Aeternitas::StorageAdapter"
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Aeternitas
|
2
|
+
class StorageAdapter
|
3
|
+
# A storage adapter that stores the entries on disk.
|
4
|
+
class File < Aeternitas::StorageAdapter
|
5
|
+
|
6
|
+
# Create a new File storage adapter.
|
7
|
+
# @param [Hash] config the adapters config
|
8
|
+
# @option config [String] :directory specifies where the entries are stored
|
9
|
+
def initialize(config)
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def store(id, raw_content)
|
14
|
+
path = file_path(id)
|
15
|
+
ensure_folders_exist(path)
|
16
|
+
raise(Aeternitas::Errors::SourceDataExists, id) if ::File.exist?(path)
|
17
|
+
::File.open(path, 'w+', encoding: 'ascii-8bit') do |f|
|
18
|
+
f.write(Zlib.deflate(raw_content, Zlib::BEST_COMPRESSION))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def retrieve(id)
|
23
|
+
raise(Aeternitas::Errors::SourceDataNotFound, id) unless exist?(id)
|
24
|
+
Zlib.inflate(::File.read(file_path(id), encoding: 'ascii-8bit'))
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete(id)
|
28
|
+
begin
|
29
|
+
!!::File.delete(file_path(id))
|
30
|
+
rescue Errno::ENOENT => e
|
31
|
+
return false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def exist?(id)
|
36
|
+
::File.exist?(file_path(id))
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the raw_content's size in bytes
|
40
|
+
# @param [String] id the entries fingerprint
|
41
|
+
# @return [Integer] the entries size in byte
|
42
|
+
def content_size(id)
|
43
|
+
retrieve(id).bytesize
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the raw_content compressed size in bytes
|
47
|
+
# @param [String] id the entries fingerprint
|
48
|
+
# @return [Integer] the entries size on disk in byte
|
49
|
+
def file_size_disk(id)
|
50
|
+
::File.size(file_path(id))
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# Calculates the location of the raw_content file given it's fingerprint.
|
56
|
+
# @param [String] id the entries fingerprint
|
57
|
+
# @return [String] the entries location
|
58
|
+
def file_path(id)
|
59
|
+
::File.join(
|
60
|
+
@config[:directory],
|
61
|
+
id[0..1], id[2..3], id[4..5],
|
62
|
+
id[6..-1]
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Makes sure that the storage location exists.
|
67
|
+
def ensure_folders_exist(path)
|
68
|
+
folders = ::File.dirname(path)
|
69
|
+
FileUtils.mkdir_p(folders) unless Dir.exist?(folders)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
require "rails/generators/active_record"
|
3
|
+
|
4
|
+
module Aeternitas
|
5
|
+
# Installs Aeternitas in a rails app.
|
6
|
+
class InstallGenerator < ::Rails::Generators::Base
|
7
|
+
include Rails::Generators::Migration
|
8
|
+
|
9
|
+
source_root File.expand_path("../templates", __FILE__)
|
10
|
+
|
11
|
+
desc 'Generates (but does not run) a migration to add all tables needed by Aeternitas.' \
|
12
|
+
' Also generates an initializer file for configuring Aeternitas'
|
13
|
+
|
14
|
+
def create_migration_file
|
15
|
+
migration_dir = File.expand_path("db/migrate")
|
16
|
+
if self.class.migration_exists?(migration_dir, 'add_aeternitas')
|
17
|
+
::Kernel.warn "Migration already exists: #{template}"
|
18
|
+
else
|
19
|
+
migration_template('add_aeternitas.rb.erb', 'db/migrate/add_aeternitas.rb')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def copy_initializer
|
24
|
+
copy_file('initializer.rb', 'config/initializers/aeternitas.rb')
|
25
|
+
end
|
26
|
+
|
27
|
+
def reminder
|
28
|
+
say "Don't forget to regularly run 'Aeternitas.enqueue_due_pollables'. E.g using 'whenever'", :red
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.next_migration_number(dirname)
|
32
|
+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# This migration creates the tables needed by Aeternitas
|
2
|
+
class AddAeternitas < ActiveRecord::Migration
|
3
|
+
def change
|
4
|
+
create_table :aeternitas_pollable_meta_data do |t|
|
5
|
+
t.string :pollable_type, null: false
|
6
|
+
t.integer :pollable_id, null: false
|
7
|
+
t.string :pollable_class, null: false
|
8
|
+
t.datetime :next_polling, null: false, default: "1970-01-01 00:00:00+002"
|
9
|
+
t.datetime :last_polling
|
10
|
+
t.string :state
|
11
|
+
t.text :deactivation_reason
|
12
|
+
t.datetime :deactivated_at
|
13
|
+
end
|
14
|
+
add_index :aeternitas_pollable_meta_data, [:pollable_id, :pollable_type], name: 'aeternitas_pollable_unique', unique: true
|
15
|
+
add_index :aeternitas_pollable_meta_data, [:next_polling, :state], name: 'aeternitas_pollable_enqueueing'
|
16
|
+
add_index :aeternitas_pollable_meta_data, [:pollable_class], name: 'aeternitas_pollable_class'
|
17
|
+
|
18
|
+
create_table :aeternitas_sources, id: :string, primary_key: :fingerprint do |t|
|
19
|
+
t.string :pollable_type, null: false
|
20
|
+
t.integer :pollable_id, null: false
|
21
|
+
t.datetime :created_at
|
22
|
+
end
|
23
|
+
add_index :aeternitas_sources, [:pollable_id, :pollable_type], name: 'aeternitas_pollable_source'
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# Configure Aeternitas
|
2
|
+
Aeternitas.configure do |config|
|
3
|
+
config.redis = { host: 'localhost', port: 6379 } #this is the default Redis config which should work in most cases.
|
4
|
+
end
|
5
|
+
|
6
|
+
Sidekiq.configure_server do |config|
|
7
|
+
config.server_middleware do |chain|
|
8
|
+
chain.add Aeternitas::Sidekiq::Middleware
|
9
|
+
end
|
10
|
+
end
|
data/logo.png
ADDED
Binary file
|
data/logo.svg
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
2
|
+
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
3
|
+
|
4
|
+
<svg
|
5
|
+
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
6
|
+
xmlns:cc="http://creativecommons.org/ns#"
|
7
|
+
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
8
|
+
xmlns:svg="http://www.w3.org/2000/svg"
|
9
|
+
xmlns="http://www.w3.org/2000/svg"
|
10
|
+
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
11
|
+
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
12
|
+
width="210mm"
|
13
|
+
height="297mm"
|
14
|
+
viewBox="0 0 210 297"
|
15
|
+
version="1.1"
|
16
|
+
id="svg8"
|
17
|
+
inkscape:version="0.92.1 r"
|
18
|
+
sodipodi:docname="Zeichnung.svg">
|
19
|
+
<defs
|
20
|
+
id="defs2" />
|
21
|
+
<sodipodi:namedview
|
22
|
+
id="base"
|
23
|
+
pagecolor="#ffffff"
|
24
|
+
bordercolor="#666666"
|
25
|
+
borderopacity="1.0"
|
26
|
+
inkscape:pageopacity="0.0"
|
27
|
+
inkscape:pageshadow="2"
|
28
|
+
inkscape:zoom="7.9195959"
|
29
|
+
inkscape:cx="156.46393"
|
30
|
+
inkscape:cy="658.66545"
|
31
|
+
inkscape:document-units="mm"
|
32
|
+
inkscape:current-layer="flowRoot3680"
|
33
|
+
showgrid="false"
|
34
|
+
inkscape:window-width="1920"
|
35
|
+
inkscape:window-height="1141"
|
36
|
+
inkscape:window-x="1920"
|
37
|
+
inkscape:window-y="0"
|
38
|
+
inkscape:window-maximized="1" />
|
39
|
+
<metadata
|
40
|
+
id="metadata5">
|
41
|
+
<rdf:RDF>
|
42
|
+
<cc:Work
|
43
|
+
rdf:about="">
|
44
|
+
<dc:format>image/svg+xml</dc:format>
|
45
|
+
<dc:type
|
46
|
+
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
47
|
+
<dc:title></dc:title>
|
48
|
+
</cc:Work>
|
49
|
+
</rdf:RDF>
|
50
|
+
</metadata>
|
51
|
+
<g
|
52
|
+
inkscape:label="Ebene 1"
|
53
|
+
inkscape:groupmode="layer"
|
54
|
+
id="layer1">
|
55
|
+
<g
|
56
|
+
aria-label="Æ"
|
57
|
+
transform="scale(0.26458333)"
|
58
|
+
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
59
|
+
id="flowRoot3680">
|
60
|
+
<path
|
61
|
+
style="fill:none;stroke:#000000;stroke-width:1.8368504;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
62
|
+
d="m 129.11306,438.19859 c -2.84852,2.31146 -6.97885,4.09709 -11.01739,4.09709 -9.00781,0 -16.31009,-6.94251 -16.31009,-15.50652 0,-8.56401 7.30228,-15.50652 16.31009,-15.50652 2.92238,0 6.18611,0.14673 8.55835,1.42648"
|
63
|
+
id="path3696"
|
64
|
+
inkscape:connector-curvature="0"
|
65
|
+
sodipodi:nodetypes="csssc"
|
66
|
+
inkscape:export-xdpi="570.17267"
|
67
|
+
inkscape:export-ydpi="570.17267" />
|
68
|
+
<path
|
69
|
+
d="M 136.61211,438.93865 H 121.74883 V 430.091 h -10 l -6.86668,12.96704 h -3.71094 l 16.12449,-32.67408 h 19.31641 v 3.16407 h -11.26953 v 8.90625 h 10.50781 v 3.125 h -10.50781 v 10.19531 h 11.26953 z M 113.13555,426.9074 h 8.61328 v -13.32031 h -2.32422 z"
|
70
|
+
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans';stroke:#000000;fill:#2c89a0;stroke-opacity:1.0"
|
71
|
+
id="path3688"
|
72
|
+
inkscape:connector-curvature="0"
|
73
|
+
sodipodi:nodetypes="cccccccccccccccccccccc"
|
74
|
+
inkscape:export-xdpi="570.17267"
|
75
|
+
inkscape:export-ydpi="570.17267" />
|
76
|
+
<path
|
77
|
+
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:none;fill-opacity:1;stroke:#000000;stroke-width:1.8368504;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
78
|
+
d="m 129.302,475.53626 c -2.84852,2.31146 -6.97885,4.09709 -11.01739,4.09709 -9.00781,0 -16.31009,-6.94251 -16.31009,-15.50652 0,-8.56401 7.30228,-15.50652 16.31009,-15.50652 2.92238,0 6.18611,0.14673 8.55835,1.42648"
|
79
|
+
id="path3696-8"
|
80
|
+
inkscape:connector-curvature="0"
|
81
|
+
sodipodi:nodetypes="csssc"
|
82
|
+
inkscape:export-xdpi="570.17267"
|
83
|
+
inkscape:export-ydpi="570.17267" />
|
84
|
+
<path
|
85
|
+
d="m 136.80105,476.27632 h -14.86328 v -8.84765 h -10 l -6.86668,12.96704 h -3.71094 l 16.12449,-32.67408 h 19.31641 v 3.16407 h -11.26953 v 8.90625 h 10.50781 v 3.125 h -10.50781 v 10.19531 h 11.26953 z m -23.47656,-12.03125 h 8.61328 v -13.32031 h -2.32422 z"
|
86
|
+
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:125%;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans';letter-spacing:0px;word-spacing:0px;fill:#2c89a0;fill-opacity:1;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1.0"
|
87
|
+
id="path3688-0"
|
88
|
+
inkscape:connector-curvature="0"
|
89
|
+
sodipodi:nodetypes="cccccccccccccccccccccc"
|
90
|
+
inkscape:export-xdpi="570.17267"
|
91
|
+
inkscape:export-ydpi="570.17267" />
|
92
|
+
<text
|
93
|
+
xml:space="preserve"
|
94
|
+
style="font-style:normal;font-weight:normal;font-size:29.65250015px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#2c89a0;fill-opacity:1;stroke:#000000;stroke-width:0.7413125px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1.0;"
|
95
|
+
x="134.15927"
|
96
|
+
y="476.14157"
|
97
|
+
id="text4524"
|
98
|
+
inkscape:export-xdpi="570.17267"
|
99
|
+
inkscape:export-ydpi="570.17267"><tspan
|
100
|
+
sodipodi:role="line"
|
101
|
+
id="tspan4522"
|
102
|
+
x="134.15927"
|
103
|
+
y="476.14157"
|
104
|
+
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Italic';letter-spacing:0px;stroke-width:0.7413125px;stroke:#000000;fill:#2c89a0;stroke-opacity:1.0;">T</tspan></text>
|
105
|
+
<text
|
106
|
+
xml:space="preserve"
|
107
|
+
style="font-style:normal;font-weight:normal;font-size:29.65250015px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#2c89a0;fill-opacity:1;stroke:#000000;stroke-width:0.7413125px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1.0;"
|
108
|
+
x="145.95291"
|
109
|
+
y="476.13583"
|
110
|
+
id="text4524-6"
|
111
|
+
inkscape:export-xdpi="570.17267"
|
112
|
+
inkscape:export-ydpi="570.17267"><tspan
|
113
|
+
sodipodi:role="line"
|
114
|
+
id="tspan4522-5"
|
115
|
+
x="145.95291"
|
116
|
+
y="476.13583"
|
117
|
+
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Italic';letter-spacing:0px;fill:#2c89a0;stroke:#000000;stroke-width:0.7413125px;stroke-opacity:1.0;">E</tspan></text>
|
118
|
+
<text
|
119
|
+
xml:space="preserve"
|
120
|
+
style="font-style:normal;font-weight:normal;font-size:29.65250015px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#2c89a0;fill-opacity:1;stroke:#000000;stroke-width:0.7413125px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1.0;"
|
121
|
+
x="156.73026"
|
122
|
+
y="476.13657"
|
123
|
+
id="text4524-0"
|
124
|
+
inkscape:export-xdpi="570.17267"
|
125
|
+
inkscape:export-ydpi="570.17267"><tspan
|
126
|
+
sodipodi:role="line"
|
127
|
+
id="tspan4522-8"
|
128
|
+
x="156.73026"
|
129
|
+
y="476.13657"
|
130
|
+
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Italic';letter-spacing:0px;fill:#2c89a0;stroke:#000000;stroke-width:0.7413125px;stroke-opacity:1.0;">R</tspan></text>
|
131
|
+
<text
|
132
|
+
xml:space="preserve"
|
133
|
+
style="font-style:normal;font-weight:normal;font-size:29.65250015px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#2c89a0;fill-opacity:1;stroke:#000000;stroke-width:0.7413125px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1.0;"
|
134
|
+
x="170.48477"
|
135
|
+
y="476.12665"
|
136
|
+
id="text4524-1"
|
137
|
+
inkscape:export-xdpi="570.17267"
|
138
|
+
inkscape:export-ydpi="570.17267"><tspan
|
139
|
+
sodipodi:role="line"
|
140
|
+
id="tspan4522-2"
|
141
|
+
x="170.48477"
|
142
|
+
y="476.12665"
|
143
|
+
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Italic';letter-spacing:0px;fill:#2c89a0;stroke:#000000;stroke-width:0.7413125px;stroke-opacity:1.0;">N</tspan></text>
|
144
|
+
<text
|
145
|
+
xml:space="preserve"
|
146
|
+
style="font-style:normal;font-weight:normal;font-size:29.65250015px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#2c89a0;fill-opacity:1;stroke:#000000;stroke-width:0.7413125px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1.0;"
|
147
|
+
x="187.77988"
|
148
|
+
y="476.1207"
|
149
|
+
id="text4524-2"
|
150
|
+
inkscape:export-xdpi="570.17267"
|
151
|
+
inkscape:export-ydpi="570.17267"><tspan
|
152
|
+
sodipodi:role="line"
|
153
|
+
id="tspan4522-6"
|
154
|
+
x="187.77988"
|
155
|
+
y="476.1207"
|
156
|
+
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Italic';letter-spacing:0px;fill:#2c89a0;stroke:#000000;stroke-width:0.7413125px;stroke-opacity:1.0;">I</tspan></text>
|
157
|
+
<text
|
158
|
+
xml:space="preserve"
|
159
|
+
style="font-style:normal;font-weight:normal;font-size:29.65250015px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#2c89a0;fill-opacity:1;stroke:#000000;stroke-width:0.7413125px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1.0;"
|
160
|
+
x="195.93016"
|
161
|
+
y="476.12857"
|
162
|
+
id="text4524-9"
|
163
|
+
inkscape:export-xdpi="570.17267"
|
164
|
+
inkscape:export-ydpi="570.17267"><tspan
|
165
|
+
sodipodi:role="line"
|
166
|
+
id="tspan4522-1"
|
167
|
+
x="195.93016"
|
168
|
+
y="476.12857"
|
169
|
+
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Italic';letter-spacing:0px;fill:#2c89a0;stroke:#000000;stroke-width:0.7413125px;stroke-opacity:1.0;">T</tspan></text>
|
170
|
+
<text
|
171
|
+
xml:space="preserve"
|
172
|
+
style="font-style:normal;font-weight:normal;font-size:29.65250015px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#2c89a0;fill-opacity:1;stroke:#000000;stroke-width:0.7413125px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1.0;"
|
173
|
+
x="204.82211"
|
174
|
+
y="476.12311"
|
175
|
+
id="text4524-04"
|
176
|
+
inkscape:export-xdpi="570.17267"
|
177
|
+
inkscape:export-ydpi="570.17267"><tspan
|
178
|
+
sodipodi:role="line"
|
179
|
+
id="tspan4522-27"
|
180
|
+
x="204.82211"
|
181
|
+
y="476.12311"
|
182
|
+
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Italic';letter-spacing:0px;fill:#2c89a0;stroke:#000000;stroke-width:0.7413125px;stroke-opacity:1.0;">A</tspan></text>
|
183
|
+
<text
|
184
|
+
xml:space="preserve"
|
185
|
+
style="font-style:normal;font-weight:normal;font-size:29.65250015px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#2c89a0;fill-opacity:1;stroke:#000000;stroke-width:0.7413125px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1.0;"
|
186
|
+
x="219.43689"
|
187
|
+
y="475.82886"
|
188
|
+
id="text4524-4"
|
189
|
+
inkscape:export-xdpi="570.17267"
|
190
|
+
inkscape:export-ydpi="570.17267"><tspan
|
191
|
+
sodipodi:role="line"
|
192
|
+
id="tspan4522-0"
|
193
|
+
x="219.43689"
|
194
|
+
y="475.82886"
|
195
|
+
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Italic';letter-spacing:0px;fill:#2c89a0;stroke:#000000;stroke-width:0.7413125px;stroke-opacity:1.0;">S</tspan></text>
|
196
|
+
</g>
|
197
|
+
</g>
|
198
|
+
</svg>
|