aeternitas 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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>
|