restful-portfolios 0.13.17

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
+ SHA256:
3
+ metadata.gz: afdeee7dc34ce903cb034e7db4c531411206f2211fe9c931cd7d85fdc46ef6a8
4
+ data.tar.gz: 39c6de3caae664a82a9b630c6c05449962ab436ac6a62461292539b46defb334
5
+ SHA512:
6
+ metadata.gz: e398a4378657e62781dae9633fc4ba0f718f5b5af5ec5229ec26bcd30a5884558affa60eaff81a65e777c32e874f5f2d1b198262ac5137dec91e4f366e9b385e
7
+ data.tar.gz: f76d75976e8744eebce9321e7edb87222207e78256d97a55e09f576d27497ed3ab92fc3f75a5d87b7b4ef37da4585cb3ba89657876633dd661a48cd0fab44db8
@@ -0,0 +1,390 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ # ######################################################################## #
5
+ #
6
+ # Main module/entry file for the Portfolios Microservice
7
+ #
8
+ # Copyright (c) 2017 Razor Risk Technologies Pty Limited. All rights reserved.
9
+ #
10
+ # ######################################################################## #
11
+
12
+
13
+ # ##########################################################################
14
+
15
+ # Microservices: Portfolios (portfolios)
16
+ #
17
+ # Supported:
18
+ #
19
+ # - [GET] / {Unsecured}
20
+ # - [DELETE] /delete/:id {Secured}
21
+ # - [GET] /get {Secured}
22
+ # - [GET] /get/:id {Secured}
23
+ # - [PUT] /put/:id {Secured}
24
+
25
+ # ##########################################################################
26
+ # requires
27
+
28
+ require 'razor_risk/cassini/diagnostics/zeroth_include'
29
+
30
+ require 'razor_risk/cassini/applications/microservices/restful/portfolios'
31
+
32
+ require 'razor_risk/cassini/applications/route_verb_adaptors/portfolios'
33
+
34
+ require 'razor_risk/cassini/main'
35
+
36
+ require 'razor_risk/cassini/applications/rest_framework/route_verb_dispatcher'
37
+ require 'razor_risk/cassini/applications/secured_microservice'
38
+ require 'razor_risk/cassini/authorisation'
39
+ require 'razor_risk/cassini/common/version'
40
+ require 'razor_risk/cassini/util/version_util'
41
+
42
+ require 'razor_risk/razor/connectivity/razor_3/header_maker'
43
+ require 'razor_risk/razor/connectivity/razor_3/razor_requester'
44
+ require 'razor_risk/razor/connectivity/version'
45
+
46
+ require 'razor_risk/core/diagnostics/extensions/libclimate'
47
+ require 'razor_risk/core/diagnostics/logger'
48
+
49
+ require 'active_support/core_ext/hash'
50
+ require 'pantheios'
51
+ require 'xqsr3/version'
52
+
53
+ require 'csv'
54
+ require 'json'
55
+ require 'nokogiri'
56
+
57
+ # ##########################################################################
58
+ # includes
59
+
60
+ include ::RazorRisk::Cassini::Applications
61
+ include ::RazorRisk::Cassini::Applications::RESTFramework
62
+ include ::RazorRisk::Cassini::Applications::RouteVerbAdaptors
63
+ include ::RazorRisk::Cassini::Authorisation::SecurityModelHelpers
64
+ include ::RazorRisk::Cassini::Constants
65
+ include ::RazorRisk::Cassini::Util::SecretsUtil
66
+ include ::RazorRisk::Cassini::Util::VersionUtil
67
+
68
+ include ::RazorRisk::Razor::Connectivity::Razor3
69
+
70
+ include ::RazorRisk::Cassini::Diagnostics
71
+
72
+ include ::Pantheios
73
+
74
+ include ::RazorRisk::Core::Diagnostics::Logger
75
+
76
+ # ##########################################################################
77
+ # constants
78
+
79
+ PROGRAM_VERSION = ::RazorRisk::Cassini::Applications::Microservices::RESTful::Portfolios::VERSION
80
+
81
+ SUPPORTED_ROUTES = [
82
+
83
+ [ '/delete/:id', :delete, 'deletes the specified portfolio', ],
84
+ [ '/get', :get, 'gets the collection of portfolios', ],
85
+ [ '/get/:id', :get, 'gets the specified portfolio', ],
86
+ [ '/put/:id', :put, 'puts the specified portfolio', ],
87
+ ]
88
+
89
+ HTTP_ACCEPTS = %w{ text/html application/json application/xml text/xml text/csv text/plain text/tab-separated-values text/tsv }
90
+
91
+ # ##########################################################################
92
+ # compatibility checks
93
+
94
+ check_version_compatibility ::RazorRisk::Cassini::Common, [ 0, 21 ], 'RazorRisk.Cassini.Common'
95
+ check_version_compatibility ::RazorRisk::Razor::Connectivity, [ 0, 11, 2 ], 'RazorRisk.Razor.Connectivity'
96
+ check_version_compatibility ::LibCLImate, '0.10'
97
+ check_version_compatibility ::Pantheios, '0.20'
98
+ check_version_compatibility ::Xqsr3::VERSION, '0.30'
99
+
100
+ # ##########################################################################
101
+ # functions
102
+
103
+ def make_CSV_routes routes
104
+
105
+ CSV.generate do |csv|
106
+
107
+ routes.each do |ar|
108
+
109
+ csv << ar
110
+ end
111
+ end
112
+ end
113
+
114
+ def make_HTML_routes routes, **options
115
+
116
+ # TODO: use erb
117
+
118
+ routes = routes.map do |ar|
119
+
120
+ <<END_OF_tr
121
+ <tr>
122
+ <td>#{ar[0]}</td>
123
+ <td>#{ar[1]}</td>
124
+ <td>#{ar[2]}</td>
125
+ </tr>
126
+ END_OF_tr
127
+ end
128
+
129
+ <<END_OF_html
130
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.we.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
131
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
132
+ <head>
133
+ <title>Razor Risk Web Service API - Internal Microservice - Portfolio</title>
134
+ <meta name="revisit-after" content="24 hours" />
135
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
136
+ </head>
137
+ <body>
138
+ <h1>Routes</h1>
139
+ <table>
140
+ <tr>
141
+ <th>Route</th>
142
+ <th>Verb</th>
143
+ <th>Description</th>
144
+ </tr>
145
+ #{routes.map { |r| r.chomp("\n") }.join("\n")}
146
+ </table>
147
+ </body>
148
+ </html>
149
+ END_OF_html
150
+ end
151
+
152
+ def make_JSON_routes routes, **options
153
+
154
+ r = {
155
+
156
+ 'routes' => []
157
+ }
158
+
159
+ r['routes'] = SUPPORTED_ROUTES.map { |ar| { route: ar[0], verb: ar[1].to_s.upcase, description: ar[2] } }
160
+
161
+ r.to_json
162
+ end
163
+
164
+ def make_Plain_routes routes
165
+
166
+ make_TSV_routes routes
167
+ end
168
+
169
+ def make_TSV_routes routes
170
+
171
+ routes.map { |ar| "#{ar[0]}\t#{ar[1]}\t#{ar[2]}\n" }
172
+ end
173
+
174
+ def make_XML_routes routes
175
+
176
+ <<END_OF_xml
177
+ <?xml version="1.0">
178
+ <routes>#{routes.map { |ar| %Q{ <route route="#{ar[0]}" verb="#{ar[1].to_s.upcase}" description="#{ar[2]}"/>}}.join(%Q{\n})}
179
+ </routes>
180
+ END_OF_xml
181
+ end
182
+
183
+ # ##########################################################################
184
+ # static set-up
185
+
186
+ # ##########################################################################
187
+ # lambdas
188
+
189
+ Lambdas_XML_to_JSON = lambda do |xml|
190
+
191
+ ConversionUtil.convert_XML_to_JSON xml
192
+ end
193
+
194
+ # ##########################################################################
195
+ # application
196
+
197
+ class PortfoliosApp < SecuredMicroservice
198
+
199
+ include RouteVerbDispatch
200
+
201
+ include ::Pantheios
202
+
203
+
204
+ FULL_DESIGNATION = 'Portfolios'
205
+ SHORT_DESIGNATION = 'portfolios'
206
+ SERVICE_TYPE = :microservice
207
+
208
+ PROGRAM_FEATURES = {
209
+
210
+ has_web_server: true,
211
+
212
+ has_host_and_port: true,
213
+
214
+ has_razor_connectivity: true,
215
+
216
+ authentication: :with_credentials_algorithm,
217
+
218
+ copyright_year: 2017,
219
+ }
220
+
221
+
222
+ def self.on_init_service options
223
+
224
+ trace ParamNames[ :options ], options
225
+
226
+ raise ArgumentError, 'missing keyword: razor_requester' unless options.has_key? :razor_requester
227
+
228
+ set :razor_requester, options[:razor_requester]
229
+ end
230
+
231
+
232
+
233
+ private
234
+ def sec
235
+
236
+ return '' if credentials.empty?
237
+ " (for #{credentials.join(':')})"
238
+ end
239
+ public
240
+
241
+
242
+ # functional routes (DELETE, GET, PUT)
243
+
244
+ delete '/delete/:id/?' do
245
+
246
+ # trace
247
+
248
+ trace ParamNames[ :request, :params ], request, params
249
+
250
+ dispatch Portfolios::ItemDelete
251
+ end
252
+
253
+ # === Query Parameters
254
+ #
255
+ # +page-base+:: [Numeric] (0) Specifies the (0-based) index index of the
256
+ # first element to be presented.
257
+ # +page-extent+:: [Numeric] (1000) Specifies the number of records to
258
+ # return in the retrieval window.
259
+ # +key-search+:: [String] Specifies a string inf the form of a series of
260
+ # keys and values to search for.
261
+ # +filter-name+:: [String] The name of a filter to use.
262
+ get '/get/hierarchy/?' do
263
+
264
+ trace ParamNames[ :request, :params ], request, params
265
+
266
+ dispatch Portfolios::HierarchyGet
267
+ end
268
+
269
+ # params:
270
+ # +result-form+:: Specifies the form of the result: +default+ gets the
271
+ # full Razor body, and is the default if none specified; +summary+ gets
272
+ # the portfolio-summary form; +portfolio+ gets the portfolio form
273
+ get '/get/:id/?' do
274
+
275
+ # trace
276
+
277
+ trace ParamNames[ :request, :params ], request, params
278
+
279
+ dispatch Portfolios::ItemGet
280
+ end
281
+
282
+ # params:
283
+ # +page-base+:: Specifies the (0-based) index index of the first element
284
+ # to be presented. Defaults to 0 if not specified
285
+ # +page-extent+:: Specifies the number of records to return in the
286
+ # retrieval window. Defaults to 1000 if not specified
287
+ # +quick-search+:: Specifies a quick-search string
288
+ # +key-search+:: Specifies a string inf the form of a series of keys and values to search for
289
+ get '/get/?' do
290
+
291
+ trace ParamNames[ :request, :params ], request, params
292
+
293
+ dispatch Portfolios::CollectionGet
294
+ end
295
+
296
+ post '/post/?' do
297
+
298
+ trace ParamNames[ :request, :params ], request, params
299
+
300
+ dispatch Portfolios::ItemPost
301
+ end
302
+
303
+ put '/put/:id/?' do
304
+
305
+ trace ParamNames[ :request, :params ], request, params
306
+
307
+ dispatch Portfolios::ItemPut
308
+ end
309
+
310
+ get '/' do
311
+
312
+ HTTP_ACCEPTS.each do |accept_type|
313
+
314
+ if request.accept? accept_type
315
+
316
+ content_type accept_type
317
+
318
+ case accept_type
319
+ when 'application/json'
320
+
321
+ r = make_JSON_routes SUPPORTED_ROUTES
322
+ when 'application/xml', 'text/xml'
323
+
324
+ r = make_XML_routes SUPPORTED_ROUTES
325
+ when 'text/csv'
326
+
327
+ r = make_CSV_routes SUPPORTED_ROUTES
328
+ when 'text/html'
329
+
330
+ r = make_HTML_routes SUPPORTED_ROUTES
331
+ when 'text/plain'
332
+
333
+ r = make_Plain_routes SUPPORTED_ROUTES
334
+ when 'text/tsv', 'text/tab-separated-values'
335
+
336
+ r = make_TSV_routes SUPPORTED_ROUTES
337
+ else
338
+
339
+ log :violation, 'unrecognised accept type \'', accept_type, '\''
340
+
341
+ halt *[ 500, {}, 'internal server failure' ]
342
+ end
343
+
344
+ return r
345
+ end
346
+ end
347
+
348
+ halt *[ 406, {}, "supports only the Accept types #{HTTP_ACCEPTS.map { |t| %Q<'#{t}'> }.join(', ')}" ]
349
+ end
350
+
351
+ define_catch_all_handlers
352
+ end
353
+
354
+ TheApp = PortfoliosApp
355
+
356
+ # now define the OPTIONS support
357
+
358
+ supported_routes = Hash.new { |h, k| h[k] = [] }
359
+
360
+ SUPPORTED_ROUTES.each do |ar|
361
+
362
+ supported_routes[ar[0]].push ar[1]
363
+ end
364
+
365
+ supported_routes.each do |k, v|
366
+
367
+ if k =~ /:id(?:\/\?)?$/
368
+
369
+ route = "#$`*"
370
+ else
371
+
372
+ route = k
373
+ end
374
+
375
+ TheApp.options route do
376
+
377
+ verbs = v.map { |t| t.upcase }.join(',')
378
+
379
+ halt *[200, { 'Allow' => verbs }, [ verbs ] ]
380
+ end
381
+ end
382
+
383
+ TheApp.options '/' do
384
+
385
+ halt *[200, { 'Allow' => 'GET' }, '' ]
386
+ end
387
+
388
+ # ############################## end of file ############################# #
389
+
390
+
@@ -0,0 +1,51 @@
1
+ # encoding: UTF-8
2
+
3
+ # ######################################################################## #
4
+ #
5
+ # Version for RazorRisk.Cassini.Microservices.RESTful.Portfolios library
6
+ #
7
+ # Copyright (c) 2019 Razor Risk Technologies Pty Limited. All rights reserved.
8
+ #
9
+ # ######################################################################## #
10
+
11
+
12
+ module RazorRisk
13
+ module Cassini
14
+ module Applications
15
+ module Microservices
16
+ module RESTful
17
+
18
+ module Portfolios
19
+
20
+ # Current version of the RazorRisk.Cassini.Microservices.RESTful.Portfolios library
21
+ VERSION = '0.13.17'
22
+
23
+ private
24
+ VERSION_PARTS_ = VERSION.split(/[.]/).collect { |n| n.to_i } # :nodoc:
25
+ public
26
+ # Major version of the RazorRisk.Cassini.Microservices.RESTful.Portfolios library
27
+ VERSION_MAJOR = VERSION_PARTS_[0] # :nodoc:
28
+ # Minor version of the RazorRisk.Cassini.Microservices.RESTful.Portfolios library
29
+ VERSION_MINOR = VERSION_PARTS_[1] # :nodoc:
30
+ # Patch version of the RazorRisk.Cassini.Microservices.RESTful.Portfolios library
31
+ VERSION_PATCH = VERSION_PARTS_[2] # :nodoc:
32
+ # Commit version of the RazorRisk.Cassini.Microservices.RESTful.Portfolios library
33
+ VERSION_COMMIT = VERSION_PARTS_[3] || 0 # :nodoc:
34
+
35
+
36
+ # The description of the framework
37
+ DESCRIPTION = "Razor Risk's Cassini Web-framework's Portfolios RESTful microservice"
38
+
39
+ # [DEPRECATED] Instead use +DESCRIPTION+
40
+ FRAMEWORK_DESCRIPTION = DESCRIPTION
41
+ end # module Portfolios
42
+
43
+ end # module RESTful
44
+ end # module Microservices
45
+ end # module Applications
46
+ end # module Cassini
47
+ end # module RazorRisk
48
+
49
+ # ############################## end of file ############################# #
50
+
51
+
@@ -0,0 +1,2 @@
1
+
2
+ require 'razor_risk/cassini/applications/microservices/restful/portfolios/version'
@@ -0,0 +1,253 @@
1
+ # encoding: UTF-8
2
+
3
+ # ######################################################################## #
4
+ #
5
+ # Adaptor for Portfolios microservice's collection GET verb
6
+ #
7
+ # Copyright (c) 2018 Razor Risk Technologies Pty Limited. All rights reserved.
8
+ #
9
+ # ######################################################################## #
10
+
11
+
12
+ # ##########################################################################
13
+ # requires
14
+
15
+ require 'razor_risk/cassini/applications/rest_framework/verb_handler'
16
+ require 'razor_risk/cassini/applications/route_verb_adaptors/utilities/collection_get_helper'
17
+ require 'razor_risk/cassini/applications/route_verb_adaptors/utilities/portfolios'
18
+
19
+ require 'razor_risk/razor/connectivity/entity_connectors/exceptions'
20
+ require 'razor_risk/razor/connectivity/razor_3/entity_connectors/portfolios_connector'
21
+
22
+ require 'razor_risk/core/diagnostics/logger'
23
+
24
+ require 'pantheios'
25
+ require 'xqsr3/conversion/integer_parser'
26
+ require 'xmlhasher'
27
+
28
+ # ##########################################################################
29
+ # module
30
+
31
+ module RazorRisk
32
+ module Cassini
33
+ module Applications
34
+ module RouteVerbAdaptors
35
+ module Portfolios
36
+
37
+ # ##########################################################################
38
+ # classes
39
+
40
+ class CollectionGet < RESTFramework::VerbHandler
41
+
42
+ include ::RazorRisk::Cassini::Applications::RouteVerbAdaptors::Utilities::CollectionGetHelper
43
+ include ::RazorRisk::Cassini::Applications::RouteVerbAdaptors::Utilities::Portfolios
44
+
45
+ include ::RazorRisk::Razor::Connectivity::EntityConnectors::Exceptions
46
+ include ::RazorRisk::Razor::Connectivity::Razor3::EntityConnectors
47
+
48
+ include ::Pantheios
49
+ include ::RazorRisk::Core::Diagnostics::Logger
50
+
51
+ HTTP_VERB = :get
52
+
53
+ HTTP_ACCEPTS = %w{ application/xml application/json text/xml }
54
+
55
+ QUERY_PARAMETERS = %w{
56
+
57
+ filter-name
58
+
59
+ page-base
60
+ page-extent
61
+
62
+ quick-search
63
+
64
+ key-search
65
+
66
+ linked-to-trade
67
+ }
68
+
69
+ def handle env, params, request, response
70
+
71
+ trace ParamNames[ :env, :params, :request, :response ], env, params, request, response
72
+
73
+ # params
74
+
75
+ filter_name = params['filter-name'].to_s.strip
76
+ filter_name = nil if filter_name.empty?
77
+ page_base = ::Xqsr3::Conversion::IntegerParser.to_integer(params['page-base'] || 0) { |x, arg|
78
+ halt 422, {}, "invalid page specifier: 'page-base'=#{arg}"
79
+ }
80
+ page_extent = ::Xqsr3::Conversion::IntegerParser.to_integer(params['page-extent'] || 1000) { |x, arg|
81
+ halt 422, {}, "invalid page specifier: 'page-extent'=#{arg}"
82
+ }
83
+ quick_search = params['quick-search']
84
+ quick_search = nil if String === quick_search && quick_search.strip.empty?
85
+ key_search = params['key-search']
86
+ key_search = nil if String === key_search && key_search.strip.empty?
87
+ linked_to_trade = params['linked-to-trade'].to_s.strip
88
+ linked_to_trade = nil if linked_to_trade.empty?
89
+
90
+ rf = case rf = (params['result-form'] || '').to_s
91
+ when '', 'default'
92
+ :body
93
+ when 'children'
94
+ :array
95
+ else
96
+ log(:warning) { "unrecognised result-form '#{rf}'" }
97
+ :body
98
+ end
99
+
100
+ if page_base < 0 || page_extent < 1
101
+
102
+ halt 416, {}, "invalid page specifier: 'page-base'=#{page_base}; 'page-extent'=#{page_extent}"
103
+ end
104
+
105
+ if filter_name and quick_search
106
+
107
+ halt 422, {}, "invalid search specifier: cannot specify 'filter-name' and 'quick-search' together"
108
+ end
109
+
110
+ if key_search
111
+ begin
112
+ key_search = MultiValueConjunctionParser.new.parse_conjunction_list key_search
113
+ rescue MultiValueConjunctionParser::SyntaxError => x
114
+ halt 422, {'Content-Type' => 'text/plain'}, x.message
115
+ end
116
+ end
117
+
118
+ rq_opts = {
119
+
120
+ :filter_name => filter_name,
121
+ :page_base => page_base,
122
+ :page_extent => page_extent,
123
+ :quick_search => quick_search,
124
+ :key_search => key_search,
125
+ :linked_to_trade => linked_to_trade,
126
+ }
127
+
128
+ # get credentials
129
+
130
+ cr = get_required_credentials
131
+
132
+ # get requester
133
+
134
+ rr = settings.razor_requester
135
+
136
+ # get connector
137
+
138
+ ec = PortfoliosConnector.new(rr, credentials: cr, **rq_opts)
139
+
140
+ # issue request
141
+
142
+ begin
143
+
144
+ qr = ec.get_portfolios indicate_result_by: :qualified_result, result_form: rf
145
+ rescue EntityConnectorInternalStepException => x
146
+
147
+ log :failure, "exception (#{x.class}): #{x}"
148
+
149
+ headers = {
150
+
151
+ 'Content-Type' => 'text/plain',
152
+ }
153
+
154
+ code = 500
155
+ msg = x.message
156
+
157
+ halt(code, headers, msg)
158
+ end
159
+
160
+ log :debug1, "qr(#{qr.class})='#{qr}'"
161
+
162
+ unless qr.succeeded?
163
+
164
+ if false
165
+
166
+ ;
167
+ elsif :no_such_filter == qr.result
168
+
169
+ log :debug1, "failed to retrieve portfolios: no such filter '#{filter_name}'"
170
+
171
+ halt 422, {}, "no such filter '#{filter_name}'"
172
+ elsif r = qr.failure_qualifier.reasons.lookup?('RZ0011')
173
+
174
+ log :debug1, 'failed to retrieve portfolios'
175
+
176
+ halt 404, {}, "#{r.message}: #{r.details}"
177
+ else
178
+
179
+ log :warning, 'failed to retrieve portfolios'
180
+
181
+ halt 500, {}, qr.failure_qualifier.reasons.join("\n")
182
+ end
183
+ end
184
+
185
+ r = case rf
186
+ when :array
187
+
188
+ xml_doc = ::Nokogiri.XML('<body/>')
189
+ root_node = xml_doc.children.first
190
+
191
+ qr.result.each do |pf_element|
192
+
193
+ root_node.add_child pf_element.to_s
194
+ end
195
+
196
+ xml_doc.to_s
197
+ else
198
+
199
+ qr.result.to_s
200
+ end
201
+
202
+ # return result
203
+
204
+ status 200
205
+
206
+ if false
207
+ elsif request.accept?('application/xml')
208
+
209
+ log :debug1, 'application/xml'
210
+
211
+ content_type 'application/xml'
212
+
213
+ r
214
+ elsif request.accept?('application/json')
215
+
216
+ log :debug1, 'application/json'
217
+
218
+ content_type 'application/json'
219
+
220
+ resp_hash = XmlHasher.parse(r)
221
+
222
+ #resp_hash = Hash.from_xml r
223
+ resp_json = resp_hash
224
+
225
+ JSON.generate resp_json
226
+ elsif request.accept?('text/xml')
227
+
228
+ log :debug1, 'text/xml'
229
+
230
+ content_type 'text/xml'
231
+
232
+ r
233
+ else
234
+
235
+ log :violation, "unexpected failure to match given 'Accept' header '#{request.accept}'"
236
+
237
+ status 500
238
+ end
239
+ end
240
+ end # class CollectionGet
241
+
242
+ # ##########################################################################
243
+ # module
244
+
245
+ end # module Portfolios
246
+ end # module RouteVerbAdaptors
247
+ end # module Applications
248
+ end # module Cassini
249
+ end # module RazorRisk
250
+
251
+ # ############################## end of file ############################# #
252
+
253
+