qb 0.3.8 → 0.3.9
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/.yardopts +4 -0
- data/exe/.qb_interop_receive +39 -0
- data/exe/qb +8 -5
- data/lib/qb.rb +64 -19
- data/lib/qb/cli/run.rb +1 -1
- data/lib/qb/github.rb +4 -0
- data/lib/qb/github/api.rb +43 -0
- data/lib/qb/github/issue.rb +83 -0
- data/lib/qb/github/repo_id.rb +127 -0
- data/lib/qb/github/resource.rb +59 -0
- data/lib/qb/github/types.rb +45 -0
- data/lib/qb/package.rb +131 -0
- data/lib/qb/package/gem.rb +2 -4
- data/lib/qb/package/version.rb +175 -2
- data/lib/qb/path.rb +2 -2
- data/lib/qb/repo.rb +137 -1
- data/lib/qb/repo/git.rb +55 -23
- data/lib/qb/repo/git/github.rb +137 -0
- data/lib/qb/role.rb +74 -30
- data/lib/qb/util.rb +1 -4
- data/lib/qb/util/docker_mixin.rb +30 -2
- data/lib/qb/util/interop.rb +68 -7
- data/lib/qb/util/logging.rb +215 -0
- data/lib/qb/util/stdio.rb +25 -9
- data/lib/qb/version.rb +16 -28
- data/library/stream +2 -2
- data/plugins/filter_plugins/ruby_interop_plugins.py +49 -31
- data/qb.gemspec +16 -4
- data/roles/qb.role/defaults/main.yml +6 -2
- data/roles/{qb.bump → qb/pkg/bump}/.qb-options.yml +0 -0
- data/roles/{qb.bump → qb/pkg/bump}/README.md +0 -0
- data/roles/{qb.bump → qb/pkg/bump}/defaults/main.yml +0 -0
- data/roles/{qb.bump → qb/pkg/bump}/library/bump +0 -0
- data/roles/{qb.bump → qb/pkg/bump}/meta/main.yml +0 -0
- data/roles/{qb.bump → qb/pkg/bump}/meta/qb.yml +0 -0
- data/roles/{qb.bump → qb/pkg/bump}/tasks/frontend/level/dev.yml +0 -0
- data/roles/{qb.bump → qb/pkg/bump}/tasks/frontend/level/rc.yml +0 -0
- data/roles/{qb.bump → qb/pkg/bump}/tasks/frontend/level/release.yml +0 -0
- data/roles/{qb.bump → qb/pkg/bump}/tasks/frontend/main.yml +0 -0
- data/roles/{qb.bump → qb/pkg/bump}/tasks/main.yml +1 -1
- data/roles/{qb.qb_role → qb/role/qb}/.qb-options.yml +0 -0
- data/roles/{qb.qb_role → qb/role/qb}/defaults/main.yml +0 -0
- data/roles/{qb.qb_role → qb/role/qb}/meta/main.yml +0 -0
- data/roles/{qb.qb_role → qb/role/qb}/meta/qb.yml +0 -0
- data/roles/{qb.qb_role → qb/role/qb}/tasks/main.yml +0 -0
- data/roles/{qb.qb_role → qb/role/qb}/templates/.gitkeep +0 -0
- data/roles/{qb.qb_role → qb/role/qb}/templates/qb.yml.j2 +0 -0
- data/roles/qb/test/rspec/spec/issue/defaults/main.yml +2 -0
- data/roles/qb/test/rspec/spec/issue/meta/main.yml +8 -0
- data/roles/qb/test/rspec/spec/issue/meta/qb.yml +67 -0
- data/roles/qb/test/rspec/spec/issue/tasks/main.yml +21 -0
- metadata +95 -26
data/lib/qb/role.rb
CHANGED
@@ -403,28 +403,40 @@ class QB::Role
|
|
403
403
|
end
|
404
404
|
|
405
405
|
|
406
|
-
#
|
406
|
+
# Do our best to figure out a role name from a path (that might not exist).
|
407
407
|
#
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
408
|
+
# We needs this for when we're creating a role.
|
409
|
+
#
|
410
|
+
# @param [String | Pathname] path
|
411
|
+
#
|
412
|
+
#
|
413
|
+
# @return [String]
|
414
|
+
#
|
415
|
+
def self.default_role_name path
|
416
|
+
resolved_path = QB::Util.resolve path
|
412
417
|
|
413
|
-
|
414
|
-
|
418
|
+
# Find the first directory in the search path that contains the path,
|
419
|
+
# if any do.
|
420
|
+
#
|
421
|
+
# It *could* be in more than one in funky situations like overlapping
|
422
|
+
# search paths or link silliness, but that doesn't matter - we consider
|
423
|
+
# the first place we find it to be the relevant once, since the search
|
424
|
+
# path is most-important-first.
|
425
|
+
#
|
426
|
+
search_dir = search_path.find { |pathname|
|
427
|
+
resolved_path.fnmatch? ( pathname / '**' ).to_s
|
415
428
|
}
|
416
429
|
|
417
|
-
|
418
|
-
when 0
|
430
|
+
if search_dir.nil?
|
419
431
|
# It's not in any of the search directories
|
420
432
|
#
|
421
433
|
# If it has 'roles' as a segment than use what's after the last occurrence
|
422
434
|
# of that (unless there isn't anything).
|
423
435
|
#
|
424
|
-
segments =
|
436
|
+
segments = resolved_path.to_s.split File::SEPARATOR
|
425
437
|
|
426
438
|
if index = segments.rindex( 'roles' )
|
427
|
-
name_segs = segments[index
|
439
|
+
name_segs = segments[( index + 1 )..( -1 )]
|
428
440
|
|
429
441
|
unless name_segs.empty?
|
430
442
|
return File.join name_segs
|
@@ -432,16 +444,15 @@ class QB::Role
|
|
432
444
|
end
|
433
445
|
|
434
446
|
# Ok, that didn't work... just return the basename I guess...
|
435
|
-
File.basename
|
436
|
-
|
437
|
-
when 1
|
438
|
-
|
439
|
-
else
|
440
|
-
# Multiple matches?!?!?
|
441
|
-
|
447
|
+
return File.basename resolved_path
|
442
448
|
|
443
449
|
end
|
444
|
-
|
450
|
+
|
451
|
+
# it's in the search path, return the relative path from the containing
|
452
|
+
# search dir to the resolved path (string version of it).
|
453
|
+
resolved_path.relative_path_from( search_dir ).to_s
|
454
|
+
|
455
|
+
end # #default_role_name
|
445
456
|
|
446
457
|
|
447
458
|
# Instance Attributes
|
@@ -871,20 +882,53 @@ class QB::Role
|
|
871
882
|
end
|
872
883
|
|
873
884
|
|
874
|
-
#
|
875
|
-
#
|
876
|
-
# `qb` for the role.
|
885
|
+
# Parsed tree structure of version requirements of the role from the
|
886
|
+
# `requirements` value in the QB meta data.
|
877
887
|
#
|
878
|
-
# @return [
|
879
|
-
#
|
888
|
+
# @return [Hash]
|
889
|
+
# Tree where the leaves are {Gem::Requirement}.
|
880
890
|
#
|
881
|
-
def
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
Gem::Requirement.new
|
891
|
+
def requirements
|
892
|
+
@requirements ||= NRSER.map_leaves(
|
893
|
+
meta_or 'requirements', {'gems' => {}}
|
894
|
+
) { |key_path, req_str|
|
895
|
+
Gem::Requirement.new req_str
|
896
|
+
}
|
897
|
+
end # #requirements
|
898
|
+
|
899
|
+
|
900
|
+
# Check the role's requirements.
|
901
|
+
#
|
902
|
+
# @return [nil]
|
903
|
+
#
|
904
|
+
# @raise [QB::AnsibleVersionError]
|
905
|
+
# If the version of Ansible found does not satisfy the role's requirements.
|
906
|
+
#
|
907
|
+
# @raise [QB::QBVersionError]
|
908
|
+
# If the the version of QB we're running does not satisfy the role's
|
909
|
+
# requirements.
|
910
|
+
#
|
911
|
+
def check_requirements
|
912
|
+
if ansible_req = requirements['ansible']
|
913
|
+
unless ansible_req.satisfied_by? QB.ansible_version
|
914
|
+
raise QB::AnsibleVersionError.squished <<-END
|
915
|
+
QB #{ QB::VERSION } requires Ansible #{ ansible_req },
|
916
|
+
found version #{ QB.ansible_version } at #{ `which ansible` }
|
917
|
+
END
|
918
|
+
end
|
886
919
|
end
|
887
|
-
|
920
|
+
|
921
|
+
if qb_req = requirements.dig( 'gems', 'qb' )
|
922
|
+
unless qb_req.satisfied_by? QB.gem_version
|
923
|
+
raise QB::QBVersionError.squished <<-END
|
924
|
+
Role #{ self } requires QB #{ qb_req },
|
925
|
+
using QB #{ QB.gem_version } from #{ QB::ROOT }.
|
926
|
+
END
|
927
|
+
end
|
928
|
+
end
|
929
|
+
|
930
|
+
nil
|
931
|
+
end # #check_requirements
|
888
932
|
|
889
933
|
|
890
934
|
# Language Inter-Op
|
data/lib/qb/util.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require_relative './util/logging'
|
1
2
|
require_relative './util/stdio'
|
2
3
|
require_relative './util/interop'
|
3
4
|
require_relative './util/bundler'
|
@@ -23,10 +24,6 @@ module QB
|
|
23
24
|
|
24
25
|
full_string_words = words full_string
|
25
26
|
|
26
|
-
QB.debug "HERE",
|
27
|
-
input_words: input_words,
|
28
|
-
full_string_words: full_string_words
|
29
|
-
|
30
27
|
full_string_words.each_with_index {|word, start_index|
|
31
28
|
# compute the end index in full_string_words
|
32
29
|
end_index = start_index + input_words.length - 1
|
data/lib/qb/util/docker_mixin.rb
CHANGED
@@ -4,14 +4,42 @@ module QB
|
|
4
4
|
module Util
|
5
5
|
|
6
6
|
# Mixin to help working with Docker.
|
7
|
-
module DockerMixin
|
7
|
+
module DockerMixin
|
8
|
+
|
9
|
+
# Character limit for Docker image tags.
|
10
|
+
#
|
11
|
+
# @return [Fixnum]
|
12
|
+
#
|
8
13
|
DOCKER_TAG_MAX_CHARACTERS = 128
|
14
|
+
|
9
15
|
|
10
|
-
# Regexp to validate strings as Docker tags
|
16
|
+
# Regexp to validate strings as Docker tags:
|
17
|
+
#
|
18
|
+
# 1. Must start with an ASCII alpha-numeric - `A-Z`, `a-z`, `0-9`.
|
19
|
+
#
|
20
|
+
# 2. The rest of the characters can be:
|
21
|
+
#
|
22
|
+
# 1. `A-Z`
|
23
|
+
# 2. `a-z`
|
24
|
+
# 3. `_`
|
25
|
+
# 4. `.`
|
26
|
+
# 5. `-`
|
27
|
+
#
|
28
|
+
# Note that it *can not* include `+`, so [Semver][] strings with
|
29
|
+
# build info after the `+` are not legal.
|
30
|
+
#
|
31
|
+
# 3. Must be {QB::Util::DockerMixin::DOCKER_TAG_MAX_CHARACTERS} in length
|
32
|
+
# or less.
|
33
|
+
#
|
34
|
+
# [Semver]: https://semver.org/
|
35
|
+
#
|
36
|
+
# @return [Regexp]
|
37
|
+
#
|
11
38
|
DOCKER_TAG_VALID_RE = \
|
12
39
|
/\A[A-Za-z0-9_][A-Za-z0-9_\.\-]{0,#{ DOCKER_TAG_MAX_CHARACTERS - 1}}\z/.
|
13
40
|
freeze
|
14
41
|
|
42
|
+
|
15
43
|
# Class methods to extend the receiver with when {QB::Util::DockerMixin}
|
16
44
|
# is included.
|
17
45
|
module ClassMethods
|
data/lib/qb/util/interop.rb
CHANGED
@@ -6,8 +6,24 @@ module QB
|
|
6
6
|
module Util
|
7
7
|
|
8
8
|
module Interop
|
9
|
+
include SemanticLogger::Loggable
|
10
|
+
|
9
11
|
class << self
|
10
12
|
|
13
|
+
def send_to_instance data, method_name, *args
|
14
|
+
logger.debug "Starting #send_to_instance..."
|
15
|
+
|
16
|
+
obj = if data.is_a?( Hash ) &&
|
17
|
+
data.key?( NRSER::Meta::Props::DEFAULT_CLASS_KEY )
|
18
|
+
NRSER::Meta::Props.UNSAFE_load_instance_from_data data
|
19
|
+
else
|
20
|
+
data
|
21
|
+
end
|
22
|
+
|
23
|
+
obj.send method_name, *args
|
24
|
+
end # #send_to_instance
|
25
|
+
|
26
|
+
|
11
27
|
# @todo Document receive method.
|
12
28
|
#
|
13
29
|
# @param [type] arg_name
|
@@ -16,28 +32,73 @@ module Interop
|
|
16
32
|
# @return [return_type]
|
17
33
|
# @todo Document return value.
|
18
34
|
#
|
19
|
-
def
|
35
|
+
def send_to_const name, method_name, *args
|
36
|
+
logger.debug "Starting #send_to_const..."
|
37
|
+
|
38
|
+
const = name.to_const
|
39
|
+
|
40
|
+
logger.debug "Found constant", const: const
|
41
|
+
|
42
|
+
const.public_send method_name, *args
|
43
|
+
|
44
|
+
end # #receive
|
45
|
+
|
46
|
+
|
47
|
+
# @todo Document receive method.
|
48
|
+
#
|
49
|
+
# @param [type] arg_name
|
50
|
+
# @todo Add name param description.
|
51
|
+
#
|
52
|
+
# @return [return_type]
|
53
|
+
# @todo Document return value.
|
54
|
+
#
|
55
|
+
def receive
|
56
|
+
logger.debug "Starting #receive..."
|
57
|
+
|
20
58
|
# method body
|
21
59
|
yaml = $stdin.read
|
22
60
|
|
23
61
|
payload = YAML.load yaml
|
24
62
|
|
25
|
-
|
63
|
+
logger.debug "Parsed",
|
64
|
+
payload: payload
|
65
|
+
|
66
|
+
# data = payload.fetch 'data'
|
26
67
|
method = payload.fetch 'method'
|
27
68
|
args = payload['args'] || []
|
28
69
|
kwds = payload['kwds'] || {}
|
29
70
|
args << kwds.symbolize_keys unless kwds.empty?
|
30
71
|
|
31
|
-
|
32
|
-
|
72
|
+
result = if payload['data']
|
73
|
+
send_to_instance payload['data'], method, *args
|
74
|
+
|
75
|
+
elsif payload['const']
|
76
|
+
send_to_const payload['const'], method, *args
|
77
|
+
|
33
78
|
else
|
34
|
-
|
79
|
+
raise ArgumentError.new binding.erb <<-ERB
|
80
|
+
Expected payload to have 'data' or 'const' keys, neither found:
|
81
|
+
|
82
|
+
Payload:
|
83
|
+
|
84
|
+
<%= payload.pretty_inspect %>
|
85
|
+
|
86
|
+
Input YAML:
|
87
|
+
|
88
|
+
<%= yaml %>
|
89
|
+
|
90
|
+
ERB
|
35
91
|
end
|
36
92
|
|
37
|
-
|
93
|
+
logger.debug "send succeeded", result: result
|
94
|
+
|
95
|
+
yaml = result.to_yaml # don't work: sort_keys: true, use_header: true
|
96
|
+
|
97
|
+
logger.debug "writing YAML:\n\n#{ yaml }"
|
38
98
|
|
39
|
-
$stdout.write
|
99
|
+
$stdout.write yaml
|
40
100
|
|
101
|
+
logger.debug "done."
|
41
102
|
end # #receive
|
42
103
|
|
43
104
|
end # class << self
|
@@ -0,0 +1,215 @@
|
|
1
|
+
# Requirements
|
2
|
+
# =======================================================================
|
3
|
+
|
4
|
+
# Stdlib
|
5
|
+
# -----------------------------------------------------------------------
|
6
|
+
|
7
|
+
# Deps
|
8
|
+
# -----------------------------------------------------------------------
|
9
|
+
require 'awesome_print'
|
10
|
+
require 'semantic_logger'
|
11
|
+
|
12
|
+
# Project / Package
|
13
|
+
# -----------------------------------------------------------------------
|
14
|
+
|
15
|
+
|
16
|
+
# Refinements
|
17
|
+
# =======================================================================
|
18
|
+
|
19
|
+
|
20
|
+
# Declarations
|
21
|
+
# =======================================================================
|
22
|
+
|
23
|
+
module QB; end
|
24
|
+
module QB::Util; end
|
25
|
+
|
26
|
+
|
27
|
+
# Definitions
|
28
|
+
# =======================================================================
|
29
|
+
|
30
|
+
# Utility methods to setup logging with [semantic_logger][].
|
31
|
+
#
|
32
|
+
# [semantic_logger]: http://rocketjob.github.io/semantic_logger/
|
33
|
+
#
|
34
|
+
module QB::Util::Logging
|
35
|
+
include SemanticLogger::Loggable
|
36
|
+
|
37
|
+
|
38
|
+
# @todo document Formatters module.
|
39
|
+
module Formatters
|
40
|
+
|
41
|
+
# Custom tweaked color formatter (for CLI output).
|
42
|
+
#
|
43
|
+
# - Turns on multiline output in Awesome Print by default.
|
44
|
+
#
|
45
|
+
class Color < SemanticLogger::Formatters::Color
|
46
|
+
|
47
|
+
# Constants
|
48
|
+
# ======================================================================
|
49
|
+
|
50
|
+
|
51
|
+
# Class Methods
|
52
|
+
# ======================================================================
|
53
|
+
|
54
|
+
|
55
|
+
# Attributes
|
56
|
+
# ======================================================================
|
57
|
+
|
58
|
+
|
59
|
+
# Constructor
|
60
|
+
# ======================================================================
|
61
|
+
|
62
|
+
# Instantiate a new `ColorFormatter`.
|
63
|
+
def initialize **options
|
64
|
+
super ap: { multiline: true },
|
65
|
+
color_map: SemanticLogger::Formatters::Color::ColorMap.new(
|
66
|
+
debug: SemanticLogger::AnsiColors::MAGENTA,
|
67
|
+
),
|
68
|
+
**options
|
69
|
+
end # #initialize
|
70
|
+
|
71
|
+
|
72
|
+
# Instance Methods
|
73
|
+
# ======================================================================
|
74
|
+
|
75
|
+
|
76
|
+
# Upcase the log level.
|
77
|
+
#
|
78
|
+
# @return [String]
|
79
|
+
#
|
80
|
+
def level
|
81
|
+
"#{ color }#{ log.level.upcase }#{ color_map.clear }"
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
# Create the log entry text. Overridden to customize appearance -
|
86
|
+
# generally reduce amount of info and put payload on it's own line.
|
87
|
+
#
|
88
|
+
# We need to replace *two* super functions, the first being
|
89
|
+
# [SemanticLogger::Formatters::Color#call][]:
|
90
|
+
#
|
91
|
+
# def call(log, logger)
|
92
|
+
# self.color = color_map[log.level]
|
93
|
+
# super(log, logger)
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# [SemanticLogger::Formatters::Color#call]: https://github.com/rocketjob/semantic_logger/blob/v4.2.0/lib/semantic_logger/formatters/color.rb#L98
|
97
|
+
#
|
98
|
+
# which doesn't do all too much, and the next being it's super-method,
|
99
|
+
# [SemanticLogger::Formatters::Default#call][]:
|
100
|
+
#
|
101
|
+
# # Default text log format
|
102
|
+
# # Generates logs of the form:
|
103
|
+
# # 2011-07-19 14:36:15.660235 D [1149:ScriptThreadProcess] Rails -- Hello World
|
104
|
+
# def call(log, logger)
|
105
|
+
# self.log = log
|
106
|
+
# self.logger = logger
|
107
|
+
#
|
108
|
+
# [time, level, process_info, tags, named_tags, duration, name, message, payload, exception].compact.join(' ')
|
109
|
+
# end
|
110
|
+
#
|
111
|
+
# [SemanticLogger::Formatters::Default#call]: https://github.com/rocketjob/semantic_logger/blob/v4.2.0/lib/semantic_logger/formatters/default.rb#L64
|
112
|
+
#
|
113
|
+
# which does most the real assembly.
|
114
|
+
#
|
115
|
+
# @param [SemanticLogger::Log] log
|
116
|
+
# The log entry to format.
|
117
|
+
#
|
118
|
+
# See [SemanticLogger::Log](https://github.com/rocketjob/semantic_logger/blob/v4.2.0/lib/semantic_logger/log.rb)
|
119
|
+
#
|
120
|
+
# @param [SemanticLogger::Logger] logger
|
121
|
+
# The logger doing the logging (pretty sure, haven't checked).
|
122
|
+
#
|
123
|
+
# See [SemanticLogger::Logger](https://github.com/rocketjob/semantic_logger/blob/v4.2.0/lib/semantic_logger/logger.rb)
|
124
|
+
#
|
125
|
+
# @return [return_type]
|
126
|
+
# @todo Document return value.
|
127
|
+
#
|
128
|
+
def call log, logger
|
129
|
+
# SemanticLogger::Formatters::Color code
|
130
|
+
self.color = color_map[log.level]
|
131
|
+
|
132
|
+
# SemanticLogger::Formatters::Default code
|
133
|
+
self.log = log
|
134
|
+
self.logger = logger
|
135
|
+
|
136
|
+
[
|
137
|
+
# time, annoyingly noisy and don't really need for local CLI app
|
138
|
+
level,
|
139
|
+
process_info,
|
140
|
+
tags,
|
141
|
+
named_tags,
|
142
|
+
duration,
|
143
|
+
name,
|
144
|
+
].compact.join( ' ' ) +
|
145
|
+
"\n" +
|
146
|
+
[
|
147
|
+
message,
|
148
|
+
payload,
|
149
|
+
exception,
|
150
|
+
].compact.join(' ') +
|
151
|
+
"\n" # I like extra newline to space shit out
|
152
|
+
|
153
|
+
end # #call
|
154
|
+
|
155
|
+
|
156
|
+
end # class Color
|
157
|
+
|
158
|
+
end # module Formatters
|
159
|
+
|
160
|
+
|
161
|
+
|
162
|
+
|
163
|
+
# Module (Class) Methods
|
164
|
+
# =====================================================================
|
165
|
+
|
166
|
+
# Setup logging.
|
167
|
+
#
|
168
|
+
# @param [type] arg_name
|
169
|
+
# @todo Add name param description.
|
170
|
+
#
|
171
|
+
# @return [return_type]
|
172
|
+
# @todo Document return value.
|
173
|
+
#
|
174
|
+
def self.setup level: nil
|
175
|
+
if level.nil?
|
176
|
+
if ENV['QB_LOG_LEVEL']
|
177
|
+
level = ENV['QB_LOG_LEVEL'].to_sym
|
178
|
+
else
|
179
|
+
level = :info
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
SemanticLogger.default_level = level
|
184
|
+
|
185
|
+
@appender ||= SemanticLogger.add_appender(
|
186
|
+
io: $stderr,
|
187
|
+
formatter: Formatters::Color.new,
|
188
|
+
)
|
189
|
+
|
190
|
+
# Set ENV vars (that Ansible modules will have access to!)
|
191
|
+
|
192
|
+
ENV['QB_LOG_LEVEL'] = level.to_s
|
193
|
+
|
194
|
+
if level == :debug
|
195
|
+
ENV['QB_DEBUG'] = 'true'
|
196
|
+
logger.debug "debug logging is ON"
|
197
|
+
else
|
198
|
+
ENV.delete 'QB_DEBUG'
|
199
|
+
end
|
200
|
+
|
201
|
+
nil
|
202
|
+
end # .setup
|
203
|
+
|
204
|
+
|
205
|
+
def self.appender
|
206
|
+
@appender
|
207
|
+
end
|
208
|
+
|
209
|
+
|
210
|
+
end # module QB::Util::Logging
|
211
|
+
|
212
|
+
|
213
|
+
|
214
|
+
# Post-Processing
|
215
|
+
# =======================================================================
|