thingfish-metastore-pg 0.1.0.pre20160627113019

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d965280761dc25444cd36fb1e0ac88099d5d3c57
4
+ data.tar.gz: f556f30655cc4012e7dd45cbc4050b2ef4c448e7
5
+ SHA512:
6
+ metadata.gz: aa6c3d6eca82612625762a33eaa1d29076863da10f7f0d04b50a03715c1a6bdc2e806b6e838505e439104018800225119fa118865e094980cef6aa72e990efc8
7
+ data.tar.gz: f0bdc7e987b43f9cbf9c7415978ddd65d0eaaaf121c919a8934b0e5df16a8a16c1a53e1e86faa536d47bd5f78dcbb1b7db859f947f6de4113eb702a631d0983e
checksums.yaml.gz.sig ADDED
@@ -0,0 +1 @@
1
+ ���e�?�Ю�=��`y$����֕�7�F����[�g���'�z򗣖��\Z�! 3=�b��]�N?r0�����c9Ǵ}�KXV�%V����xO4���H?Cqܻ�s~J'z�H��yU5�R7���X�Xҧ�@WsL���2la50/���)8��V�Ȋ�L���;�\ix3G���+�&�5_׋j� �&G؜7|2Ux�0�$ll���gG��p#K`r�u�>��f� �*n9� A;l��M�7��-�����#�_ii�?�ǐG7�b���]�x����ƾ$�O��y�~O�#�y
data.tar.gz.sig ADDED
Binary file
data/.document ADDED
@@ -0,0 +1,4 @@
1
+ lib/**/*.rb
2
+ README.rdoc
3
+ ChangeLog.rdoc
4
+ LICENSE.txt
data/.simplecov ADDED
@@ -0,0 +1,9 @@
1
+ # Simplecov config
2
+
3
+ SimpleCov.start do
4
+ add_filter 'spec'
5
+ add_filter 'integration'
6
+ add_group "Needing tests" do |file|
7
+ file.covered_percent < 90
8
+ end
9
+ end
data/ChangeLog ADDED
@@ -0,0 +1,95 @@
1
+ 2016-06-27 Michael Granger <ged@FaerieMUD.org>
2
+
3
+ * .gems, .ruby-gemset, .ruby-version, .rvmrc, Gemfile, History.md,
4
+ History.rdoc, README.md, README.rdoc, Rakefile, thingfish-metastore-
5
+ pg.gemspec:
6
+ Update build env
7
+ [0a1c87687905] [tip]
8
+
9
+ 2015-12-14 Michael Granger <ged@FaerieMUD.org>
10
+
11
+ * lib/thingfish/metastore/pg.rb,
12
+ lib/thingfish/metastore/pg/metadata.rb:
13
+ Consider user metadata for ordering, too
14
+ [d69c38e41d05] [github/master]
15
+
16
+ 2015-12-10 Michael Granger <ged@FaerieMUD.org>
17
+
18
+ * lib/thingfish/metastore/pg.rb:
19
+ Don't raise a NoMethodError on missing metadata keys
20
+ [99ac5402317a]
21
+
22
+ * Rakefile:
23
+ Use latest semantic versioning for the gemspec
24
+ [7c7236fda1e2]
25
+
26
+ 2015-11-02 Mahlon E. Smith <mahlon@martini.nu>
27
+
28
+ * lib/thingfish/metastore/pg.rb:
29
+ Rename obsoleted metastore API to correct name.
30
+ [993664929f87]
31
+
32
+ * LICENSE.rdoc, README.rdoc, Rakefile:
33
+ Fix README and LICENSE file.
34
+ [e9c83aeb5889]
35
+
36
+ 2015-04-01 Mahlon E. Smith <mahlon@martini.nu>
37
+
38
+ * .gems, .rvm.gems, .rvmrc:
39
+ Bump development default ruby version, rename rvm gems file.
40
+ [3e762af58fc9]
41
+
42
+ 2015-01-29 Michael Granger <ged@FaerieMUD.org>
43
+
44
+ * lib/thingfish/metastore/pg.rb:
45
+ Fix the #apply_search_order method
46
+ [41dec81585a4]
47
+
48
+ * lib/thingfish/metastore/pg.rb:
49
+ Make the return from #search not use a lazy enumerator.
50
+
51
+ It was an interesting experiment, but I don't think it's going to
52
+ work without retooling the handler to support enumerators.
53
+ [7e3215bec4f4]
54
+
55
+ * lib/thingfish/metastore/pg/metadata.rb:
56
+ Ensure the "uploadaddress" is a string
57
+ [e367a26e6d3b]
58
+
59
+ * lib/thingfish/metastore/pg.rb:
60
+ Rearrange the config methods.
61
+
62
+ The `::configure` method needs to be below the methods it calls in
63
+ case it's called upon definition.
64
+ [485bf6854980]
65
+
66
+ * .hgignore:
67
+ Ignore the package directory.
68
+ [21423daeb9ce]
69
+
70
+ 2015-01-28 Michael Granger <ged@FaerieMUD.org>
71
+
72
+ * data/thingfish-metastore-pg/migrations/20150114_initial.rb,
73
+ lib/thingfish/metastore/pg.rb,
74
+ lib/thingfish/metastore/pg/metadata.rb, spec/spec_helper.rb:
75
+ Add initial Thingfish::Metastore API
76
+ [3b0d1b844ad6]
77
+
78
+ * .hgignore, .pryrc, Manifest.txt:
79
+ Update some project files
80
+ [95106fe17011]
81
+
82
+ 2015-01-14 Michael Granger <ged@FaerieMUD.org>
83
+
84
+ * .rvm.gems, data/thingfish-metastore-
85
+ pg/migrations/20150114_initial.rb, lib/thingfish/metastore/pg.rb,
86
+ spec/spec_helper.rb, spec/thingfish/metastore/pg_spec.rb:
87
+ Got the schema loading and spec helpers set up
88
+ [62f8eaf03619]
89
+
90
+ * .document, .gitignore, .hgignore, .pryrc, .rvm.gems, .rvmrc,
91
+ .simplecov, History.rdoc, LICENSE.rdoc, README.rdoc, Rakefile,
92
+ lib/thingfish/metastore/pg.rb, spec/spec_helper.rb,
93
+ spec/thingfish/metastore/pg_spec.rb:
94
+ Initial boilerplate
95
+ [d4cb46c1f0b5]
data/History.md ADDED
@@ -0,0 +1,4 @@
1
+ ## v0.0.1 [YYYY-MM-DD] Michael Granger <ged@FaerieMUD.org>
2
+
3
+ Initial release.
4
+
data/LICENSE.md ADDED
@@ -0,0 +1,29 @@
1
+ Copyright (c) 2014-2016, Michael Granger and Mahlon E. Smith.
2
+
3
+ All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without modification, are
6
+ permitted provided that the following conditions are met:
7
+
8
+ * Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ * Redistributions in binary form must reproduce the above copyright notice, this
12
+ list of conditions and the following disclaimer in the documentation and/or
13
+ other materials provided with the distribution.
14
+
15
+ * Neither the name of the authors, nor the names of its contributors may be used to
16
+ endorse or promote products derived from this software without specific prior
17
+ written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
23
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
24
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
25
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
26
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
27
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
28
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/Manifest.txt ADDED
@@ -0,0 +1,13 @@
1
+ .document
2
+ .simplecov
3
+ ChangeLog
4
+ History.md
5
+ LICENSE.md
6
+ Manifest.txt
7
+ README.md
8
+ Rakefile
9
+ data/thingfish-metastore-pg/migrations/20150114_initial.rb
10
+ lib/thingfish/metastore/pg.rb
11
+ lib/thingfish/metastore/pg/metadata.rb
12
+ spec/spec_helper.rb
13
+ spec/thingfish/metastore/pg_spec.rb
data/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # Thingfish PostgreSQL Metastore
2
+
3
+ home
4
+ : https://bitbucket.org/projects/thingfish-metastore-pg
5
+
6
+ code
7
+ : https://bitbucket.org/ged/thingfish-metastore-pg
8
+
9
+ github
10
+ : https://github.com/ged/thingfish-metastore-pg
11
+
12
+ docs
13
+ : https://deveiate.org/code/thingfish-metastore-pg
14
+
15
+
16
+ ## Description
17
+
18
+ This is a metadata storage plugin for the Thingfish digital asset manager. It provides persistent storage for uploaded data to a PostgreSQL table.
19
+
20
+
21
+ ## Authors
22
+
23
+ * Michael Granger <ged@FaerieMUD.org>
24
+ * Mahlon E. Smith <mahlon@martini.nu>
25
+
26
+
27
+ ## Installation
28
+
29
+ $ gem install thingfish-metastore-pg
30
+
31
+
32
+ ## Usage
33
+
34
+ As with Thingfish itself, this plugin uses Configurability[https://rubygems.org/gems/configurability] to modify default behaviors.
35
+
36
+ Here's an example configuration file that enables this plugin.
37
+
38
+ ---
39
+ thingfish:
40
+ metastore: pg
41
+
42
+ pg_metastore:
43
+ uri: postgres://thingfish:password@db.example.com/database
44
+
45
+
46
+ When Thingfish starts, it will install the necessary database schema automatically.
47
+
48
+
49
+ ## License
50
+
51
+ Copyright (c) 2014-2016, Michael Granger and Mahlon E. Smith.
52
+
53
+ All rights reserved.
54
+
55
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
56
+
57
+ * Redistributions of source code must retain the above copyright notice, this
58
+ list of conditions and the following disclaimer.
59
+
60
+ * Redistributions in binary form must reproduce the above copyright notice, this
61
+ list of conditions and the following disclaimer in the documentation and/or
62
+ other materials provided with the distribution.
63
+
64
+ * Neither the name of the authors, nor the names of its contributors may be used to
65
+ endorse or promote products derived from this software without specific prior
66
+ written permission.
67
+
68
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
69
+
data/Rakefile ADDED
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env rake
2
+
3
+ begin
4
+ require 'hoe'
5
+ rescue LoadError
6
+ abort "This Rakefile requires hoe (gem install hoe)"
7
+ end
8
+
9
+ GEMSPEC = 'thingfish-metastore-pg.gemspec'
10
+
11
+
12
+ Hoe.plugin :mercurial
13
+ Hoe.plugin :signing
14
+ Hoe.plugin :deveiate
15
+ Hoe.plugin :bundler
16
+
17
+ Hoe.plugins.delete :rubyforge
18
+ Hoe.plugins.delete :gemcutter
19
+
20
+ hoespec = Hoe.spec 'thingfish-metastore-pg' do |spec|
21
+ spec.readme_file = 'README.md'
22
+ spec.history_file = 'History.md'
23
+ spec.extra_rdoc_files = FileList[ '*.rdoc', '*.md' ]
24
+ spec.license 'BSD-3-Clause'
25
+ spec.urls = {
26
+ home: 'https://bitbucket.org/ged/thingfish-metastore-pg',
27
+ code: 'https://bitbucket.org/ged/thingfish-metastore-pg',
28
+ docs: 'https://deveiate.org/code/thingfish-metastore-pg',
29
+ github: 'https://github.com/ged/thingfish-metastore-pg',
30
+ }
31
+
32
+ spec.developer 'Michael Granger', 'ged@FaerieMUD.org'
33
+ spec.developer 'Mahlon E. Smith', 'mahlon@martini.nu'
34
+
35
+ spec.dependency 'thingfish', '~> 0.5'
36
+ spec.dependency 'loggability', '~> 0.10'
37
+ spec.dependency 'configurability', '~> 2.2'
38
+ spec.dependency 'sequel', '~> 4.35'
39
+ spec.dependency 'pg', '~> 0.18'
40
+
41
+ spec.dependency 'rspec', '~> 3.0', :developer
42
+
43
+ spec.require_ruby_version( '>=2.3.0' )
44
+ spec.hg_sign_tags = true if spec.respond_to?( :hg_sign_tags= )
45
+
46
+ self.rdoc_locations << "deveiate:/usr/local/www/public/code/#{remote_rdoc_dir}"
47
+ end
48
+
49
+
50
+ ENV['VERSION'] ||= hoespec.spec.version.to_s
51
+
52
+ # Run the tests before checking in
53
+ task 'hg:precheckin' => [ :check_history, :check_manifest, :gemspec, :spec ]
54
+
55
+ # Rebuild the ChangeLog immediately before release
56
+ task :prerelease => 'ChangeLog'
57
+ CLOBBER.include( 'ChangeLog' )
58
+
59
+ desc "Build a coverage report"
60
+ task :coverage do
61
+ ENV["COVERAGE"] = 'yes'
62
+ Rake::Task[:spec].invoke
63
+ end
64
+
65
+
66
+ # Use the fivefish formatter for docs generated from development checkout
67
+ if File.directory?( '.hg' )
68
+ require 'rdoc/task'
69
+
70
+ Rake::Task[ 'docs' ].clear
71
+ RDoc::Task.new( 'docs' ) do |rdoc|
72
+ rdoc.main = "README.md"
73
+ rdoc.rdoc_files.include( "*.rdoc", "*.md", "ChangeLog", "lib/**/*.rb" )
74
+ rdoc.generator = :fivefish
75
+ rdoc.title = 'Arborist'
76
+ rdoc.rdoc_dir = 'doc'
77
+ end
78
+ end
79
+
80
+
81
+ task :gemspec => GEMSPEC
82
+ file GEMSPEC => [ __FILE__, 'ChangeLog' ] do |task|
83
+ spec = $hoespec.spec
84
+ spec.files.delete( '.gemtest' )
85
+ spec.signing_key = nil
86
+ spec.version = "#{spec.version.bump}.0.pre#{Time.now.strftime("%Y%m%d%H%M%S")}"
87
+ File.open( task.name, 'w' ) do |fh|
88
+ fh.write( spec.to_ruby )
89
+ end
90
+ end
91
+
92
+ task :default => :gemspec
93
+ CLOBBER.include( GEMSPEC.to_s )
@@ -0,0 +1,23 @@
1
+ # vim: set nosta noet ts=4 sw=4:
2
+
3
+ ### The initial Thingfish::Metastore::PG DDL.
4
+ ###
5
+ class Initial < Sequel::Migration
6
+
7
+ def up
8
+ create_schema( :thingfish, if_not_exists: true )
9
+ create_table( :thingfish__metadata ) do
10
+ uuid :oid, primary_key: true
11
+ text :format, null: false
12
+ int :extent, null: false
13
+ timestamptz :created, null: false, default: Sequel.function(:now)
14
+ inet :uploadaddress, null: false
15
+ jsonb :user_metadata, null: false, default: '{}'
16
+ end
17
+ end
18
+
19
+ def down
20
+ drop_table( :thingfish__metadata )
21
+ end
22
+ end
23
+
@@ -0,0 +1,305 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'loggability'
5
+ require 'configurability'
6
+ require 'sequel'
7
+ require 'strelka'
8
+ require 'strelka/mixins'
9
+
10
+ require 'thingfish'
11
+ require 'thingfish/mixins'
12
+ require 'thingfish/metastore'
13
+
14
+ # Toplevel namespace
15
+ class Thingfish::Metastore::PG < Thingfish::Metastore
16
+ extend Loggability,
17
+ Configurability,
18
+ Strelka::MethodUtilities
19
+ include Thingfish::Normalization
20
+
21
+
22
+ # Load Sequel extensions/plugins
23
+ Sequel.extension :migration
24
+
25
+
26
+ # Package version
27
+ VERSION = '0.0.1'
28
+
29
+ # Version control revision
30
+ REVISION = %q$Revision: d69c38e41d05 $
31
+
32
+ # The data directory that contains migration files.
33
+ #
34
+ DATADIR = if ENV['THINGFISH_METASTORE_PG_DATADIR']
35
+ Pathname.new( ENV['THINGFISH_METASTORE_PG_DATADIR'] )
36
+ elsif Gem.datadir( 'thingfish-metastore-pg' )
37
+ Pathname.new( Gem.datadir('thingfish-metastore-pg') )
38
+ else
39
+ Pathname.new( __FILE__ ).dirname.parent.parent.parent +
40
+ 'data' + 'thingfish-metastore-pg'
41
+ end
42
+
43
+ # The default config values
44
+ DEFAULT_CONFIG = {
45
+ uri: 'postgres:/thingfish',
46
+ slow_query_seconds: 0.01,
47
+ }
48
+
49
+
50
+ # Loggability API -- use a separate logger
51
+ log_as :thingfish_metastore_pg
52
+
53
+ # Configurability API -- load the `pg_metastore`
54
+ config_key :pg_metastore
55
+
56
+ ##
57
+ # The URI of the database to use for the metastore
58
+ singleton_attr_accessor :uri
59
+
60
+ ##
61
+ # The Sequel::Database that's used to access the metastore tables
62
+ singleton_attr_accessor :db
63
+
64
+ ##
65
+ # The number of seconds to consider a "slow" query
66
+ singleton_attr_accessor :slow_query_seconds
67
+
68
+
69
+ ### Set up the metastore database and migrate to the latest version.
70
+ def self::setup_database
71
+ Sequel.extension :pg_json_ops
72
+
73
+ self.db = Sequel.connect( self.uri )
74
+
75
+ self.db.logger = Loggability[ Thingfish::Metastore::PG ]
76
+ self.db.sql_log_level = :debug
77
+ self.db.extension( :pg_json )
78
+ self.db.log_warn_duration = self.slow_query_seconds
79
+
80
+ # Ensure the database is current.
81
+ #
82
+ unless Sequel::Migrator.is_current?( self.db, self.migrations_dir.to_s )
83
+ self.log.info "Installing database schema..."
84
+ Sequel::Migrator.apply( self.db, self.migrations_dir.to_s )
85
+ end
86
+ end
87
+
88
+
89
+ ### Tear down the configured metastore database.
90
+ def self::teardown_database
91
+ self.log.info "Tearing down database schema..."
92
+ Sequel::Migrator.apply( self.db, self.migrations_dir.to_s, 0 )
93
+ end
94
+
95
+
96
+ ### Return the current database migrations directory as a Pathname
97
+ def self::migrations_dir
98
+ return DATADIR + 'migrations'
99
+ end
100
+
101
+
102
+ ### Configurability API -- set up the metastore with the `pg_metastore` section of
103
+ ### the config file.
104
+ def self::configure( config=nil )
105
+ config = self.defaults.merge( config || {} )
106
+
107
+ self.uri = config[:uri]
108
+ self.slow_query_seconds = config[:slow_query_seconds]
109
+
110
+ self.setup_database
111
+ end
112
+
113
+
114
+ ### Set up the metastore.
115
+ def initialize( * ) # :notnew:
116
+ require 'thingfish/metastore/pg/metadata'
117
+ Thingfish::Metastore::PG::Metadata.db = self.class.db
118
+ @model = Thingfish::Metastore::PG::Metadata
119
+ end
120
+
121
+
122
+ ######
123
+ public
124
+ ######
125
+
126
+ ##
127
+ # The Sequel model representing the metadata rows.
128
+ attr_reader :model
129
+
130
+
131
+ #
132
+ # :section: Thingfish::Metastore API
133
+ #
134
+
135
+ ### Return an Array of all stored oids.
136
+ def oids
137
+ return self.each_oid.to_a
138
+ end
139
+
140
+
141
+ ### Iterate over each of the store's oids, yielding to the block if one is given
142
+ ### or returning an Enumerator if one is not.
143
+ def each_oid( &block )
144
+ return self.model.select_map( :oid ).each( &block )
145
+ end
146
+
147
+
148
+ ### Save the +metadata+ Hash for the specified +oid+.
149
+ def save( oid, metadata )
150
+ md = self.model.from_hash( metadata )
151
+ md.oid = oid
152
+ md.save
153
+ end
154
+
155
+
156
+ ### Fetch the data corresponding to the given +oid+ as a Hash-ish object.
157
+ def fetch( oid, *keys )
158
+ metadata = self.model[ oid ] or return nil
159
+
160
+ if keys.empty?
161
+ return metadata.to_hash
162
+ else
163
+ keys = normalize_keys( keys )
164
+ values = metadata.to_hash.values_at( *keys )
165
+ return Hash[ [keys, values].transpose ]
166
+ end
167
+ end
168
+
169
+
170
+ ### Fetch the value of the metadata associated with the given +key+ for the
171
+ ### specified +oid+.
172
+ def fetch_value( oid, key )
173
+ metadata = self.model[ oid ] or return nil
174
+ key = key.to_sym
175
+ return metadata[ key ] || metadata.user_metadata[ key ]
176
+ end
177
+
178
+
179
+ ### Fetch UUIDs related to the given +oid+.
180
+ def fetch_related_oids( oid )
181
+ oid = normalize_oid( oid )
182
+ return self.model.related_to( oid ).select_map( :oid )
183
+ end
184
+
185
+
186
+ ### Search the metastore for UUIDs which match the specified +criteria+ and
187
+ ### return them as an iterator.
188
+ def search( options={} )
189
+ ds = self.model.naked.select( :oid )
190
+ self.log.debug "Starting search with %p" % [ ds ]
191
+
192
+ ds = self.omit_related_resources( ds, options )
193
+ ds = self.apply_search_criteria( ds, options )
194
+ ds = self.apply_search_order( ds, options )
195
+ ds = self.apply_search_direction( ds, options )
196
+ ds = self.apply_search_limit( ds, options )
197
+
198
+ self.log.debug "Dataset for search is: %s" % [ ds.sql ]
199
+
200
+ return ds.map {|row| row[:oid] }
201
+ end
202
+
203
+
204
+ ### Update the metadata for the given +oid+ with the specified +values+ hash.
205
+ def merge( oid, values )
206
+ values = normalize_keys( values )
207
+
208
+ md = self.model[ oid ] or return nil
209
+ md.merge!( values )
210
+ md.save
211
+ end
212
+
213
+
214
+ ### Remove all metadata associated with +oid+ from the Metastore.
215
+ def remove( oid, *keys )
216
+ self.model[ oid: oid ].destroy
217
+ end
218
+
219
+
220
+ ### Remove all metadata associated with +oid+ except for the specified +keys+.
221
+ def remove_except( oid, *keys )
222
+ keys = normalize_keys( keys )
223
+
224
+ md = self.model[ oid ] or return nil
225
+ md.user_metadata.keep_if {|key,_| keys.include?(key) }
226
+ md.save
227
+ end
228
+
229
+
230
+ ### Returns +true+ if the metastore has metadata associated with the specified +oid+.
231
+ def include?( oid )
232
+ return self.model.count( oid: oid ).nonzero?
233
+ end
234
+
235
+
236
+ ### Returns the number of objects the store contains.
237
+ def size
238
+ return self.model.count
239
+ end
240
+
241
+
242
+ #########
243
+ protected
244
+ #########
245
+
246
+ ### Omit related resources from the search dataset +ds+ unless the given
247
+ ### +options+ specify otherwise.
248
+ def omit_related_resources( ds, options )
249
+ unless options[:include_related]
250
+ self.log.debug " omitting entries for related resources"
251
+ ds = ds.unrelated
252
+ end
253
+ return ds
254
+ end
255
+
256
+
257
+ ### Apply the search :criteria from the specified +options+ to the collection
258
+ ### in +ds+ and return the modified dataset.
259
+ def apply_search_criteria( ds, options )
260
+ if (( criteria = options[:criteria] ))
261
+ criteria.each do |field, value|
262
+ self.log.debug " applying criteria: %p => %p" % [ field.to_s, value ]
263
+ ds = ds.where_metadata( field => value )
264
+ end
265
+ end
266
+
267
+ return ds
268
+ end
269
+
270
+
271
+ ### Apply the search :order from the specified +options+ to the collection in
272
+ ### +ds+ and return the modified dataset.
273
+ def apply_search_order( ds, options )
274
+ if options[:order]
275
+ columns = Array( options[:order] )
276
+ self.log.debug " ordering results by columns: %p" % [ columns ]
277
+ ds = ds.order_metadata( columns )
278
+ end
279
+
280
+ return ds
281
+ end
282
+
283
+
284
+ ### Apply the search :direction from the specified +options+ to the collection
285
+ ### in +ds+ and return the modified dataset.
286
+ def apply_search_direction( ds, options )
287
+ ds = ds.reverse if options[:direction] && options[:direction] == 'desc'
288
+ return ds
289
+ end
290
+
291
+
292
+ ### Apply the search :limit from the specified +options+ to the collection in
293
+ ### +ds+ and return the modified dataset.
294
+ def apply_search_limit( ds, options )
295
+ if (( limit = options[:limit] ))
296
+ self.log.debug " limiting to %s results" % [ limit ]
297
+ offset = options[:offset] || 0
298
+ ds = ds.limit( limit, offset )
299
+ end
300
+
301
+ return ds
302
+ end
303
+
304
+ end # class Thingfish::Metastore::PG
305
+
@@ -0,0 +1,154 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'sequel/model'
5
+
6
+ require 'thingfish/mixins'
7
+ require 'thingfish/metastore/pg' unless defined?( Thingfish::Metastore::PG )
8
+
9
+
10
+ # A row of metadata describing an asset in a Thingfish store.
11
+ class Thingfish::Metastore::PG::Metadata < Sequel::Model( :thingfish__metadata )
12
+ include Thingfish::Normalization
13
+
14
+ # Allow instances to be created with a primary key
15
+ unrestrict_primary_key
16
+
17
+
18
+ # Dataset methods
19
+ dataset_module do
20
+
21
+ ### Dataset method: Limit results to metadata which is for a related resource.
22
+ def related
23
+ return self.exclude( self.user_metadata_expr('relation') => nil )
24
+ end
25
+
26
+
27
+ ### Dataset method: Limit results to metadata which is not for a related resource.
28
+ def unrelated
29
+ return self.where_metadata( relation: nil )
30
+ end
31
+
32
+
33
+ ### Return a dataset which will select metadata of resources related to the
34
+ ### resource with the given +oid+.
35
+ def related_to( oid )
36
+ oid = oid.oid if oid.respond_to?( :oid ) # Support query by model object
37
+ oid = Thingfish::Normalization.normalize_oid( oid )
38
+
39
+ return self.where_metadata( relation: oid )
40
+ end
41
+
42
+
43
+ ### Dataset method: Limit results to records whose operational or user
44
+ ### metadata matches the values from the specified +hash+.
45
+ def where_metadata( hash )
46
+ ds = self
47
+ hash.each do |field, value|
48
+ if Thingfish::Metastore::PG::Metadata.metadata_columns.include?( field.to_sym )
49
+ ds = ds.where( field.to_sym => value )
50
+ else
51
+ ds = ds.where( self.user_metadata_expr(field) => value )
52
+ end
53
+ end
54
+
55
+ return ds
56
+ end
57
+
58
+
59
+ ### Dataset method: Order results by the specified +columns+.
60
+ def order_metadata( *columns )
61
+ columns.flatten!
62
+ ds = self
63
+ columns.each do |column|
64
+ if Thingfish::Metastore::PG::Metadata.metadata_columns.include?( column.to_sym )
65
+ ds = ds.order_append( column.to_sym )
66
+ else
67
+ ds = ds.order_append( self.user_metadata_expr(column) )
68
+ end
69
+ end
70
+
71
+ return ds
72
+ end
73
+
74
+
75
+ #########
76
+ protected
77
+ #########
78
+
79
+ ### Returns a Sequel expression suitable for use as the key of a query against
80
+ ### the specified user metadata field.
81
+ def user_metadata_expr( field )
82
+ return Sequel.pg_jsonb( :user_metadata ).get_text( field.to_s )
83
+ end
84
+
85
+ end # dataset_module
86
+
87
+
88
+ ### Return a new Metadata object from the given +oid+ and one-dimensional +hash+
89
+ ### used by Thingfish.
90
+ def self::from_hash( hash )
91
+ metadata = Thingfish::Normalization.normalize_keys( hash )
92
+
93
+ md = new
94
+
95
+ md.format = metadata.delete( 'format' )
96
+ md.extent = metadata.delete( 'extent' )
97
+ md.created = metadata.delete( 'created' )
98
+ md.uploadaddress = metadata.delete( 'uploadaddress' ).to_s
99
+
100
+ md.user_metadata = Sequel.pg_jsonb( metadata )
101
+
102
+ return md
103
+ end
104
+
105
+
106
+ ### Return the columns of the table that are used for resource metadata.
107
+ def self::metadata_columns
108
+ return self.columns - [self.primary_key, :user_metadata]
109
+ end
110
+
111
+
112
+ ### Do some initial attribute setup for new objects.
113
+ def initialize( * )
114
+ super
115
+ self[ :user_metadata ] ||= Sequel.pg_jsonb({})
116
+ end
117
+
118
+
119
+ ### Return the metadata as a Hash; overridden from Sequel::Model to
120
+ ### merge the user and system pairs together.
121
+ def to_hash
122
+ hash = self.values
123
+
124
+ hash.delete( :oid )
125
+ hash.merge!( hash.delete(:user_metadata) )
126
+
127
+ return normalize_keys( hash )
128
+ end
129
+
130
+
131
+ ### Merge new metadata +values+ into the metadata for the resource
132
+ def merge!( values )
133
+
134
+ # Extract and set the column-metadata values first
135
+ self.class.metadata_columns.each do |col|
136
+ next unless values.key?( col.to_s )
137
+ self[ col ] = values.delete( col.to_s )
138
+ end
139
+
140
+ self.user_metadata.merge!( values )
141
+ end
142
+
143
+
144
+ #########
145
+ protected
146
+ #########
147
+
148
+ ### Proxy method -- fetch a value from the metadata hash if it exists.
149
+ def method_missing( sym, *args, &block )
150
+ return self.user_metadata[ sym.to_s ] || super
151
+ end
152
+
153
+ end # Thingfish::Metastore::PG::Metadata
154
+
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/ruby
2
+ # coding: utf-8
3
+
4
+ BEGIN {
5
+ require 'pathname'
6
+ basedir = Pathname( __FILE__ ).dirname.parent
7
+
8
+ thingfishdir = basedir.parent + 'Thingfish'
9
+ thingfishlib = thingfishdir + 'lib'
10
+
11
+ $LOAD_PATH.unshift( thingfishlib.to_s ) if thingfishlib.exist?
12
+ }
13
+
14
+
15
+ # SimpleCov test coverage reporting; enable this using the :coverage rake task
16
+ require 'simplecov' if ENV['COVERAGE']
17
+
18
+ require 'loggability'
19
+ require 'loggability/spechelpers'
20
+ require 'configurability'
21
+ require 'configurability/behavior'
22
+
23
+ require 'rspec'
24
+ require 'thingfish'
25
+ require 'thingfish/spechelpers'
26
+ require 'thingfish/behaviors'
27
+ require 'thingfish/metastore'
28
+
29
+ Loggability.format_with( :color ) if $stdout.tty?
30
+
31
+ # Some helper functions for testing. Usage:
32
+ #
33
+ # # in spec/spec_helper.rb
34
+ # RSpec.configure do |c|
35
+ # c.include( Thingfish::Metastore::PG::SpecHelpers )
36
+ # end
37
+ #
38
+ # # in my_class_spec.rb; mark an example as needing database setup
39
+ # describe MyClass, db: true do
40
+ # end
41
+ #
42
+ module Thingfish::MetastorePGSpecHelpers
43
+
44
+ TESTDB_ENV_VAR = 'THINGFISH_DB_URI'
45
+
46
+ ### Inclusion callback -- install some hooks
47
+ def self::included( context )
48
+
49
+ context.before( :all ) do
50
+ if ((db_uri = ENV[ TESTDB_ENV_VAR ]))
51
+ Thingfish::Metastore::PG.configure( uri: db_uri )
52
+ end
53
+ end
54
+
55
+ context.after( :all ) do
56
+ Thingfish::Metastore::PG.teardown_database if Thingfish::Metastore::PG.db
57
+ end
58
+
59
+ context.around( :each ) do |example|
60
+ if (( setting = example.metadata[:db] ))
61
+ Loggability[ Thingfish::Metastore::PG ].debug "DB setting: %p" % [ setting ]
62
+
63
+ if ((db = Thingfish::Metastore::PG.db))
64
+ if setting == :no_transaction || setting == :without_transaction
65
+ Loggability[ Thingfish::Metastore::PG ].debug " running without a transaction"
66
+ example.run
67
+ else
68
+ Loggability[ Thingfish::Metastore::PG ].debug " running with a transaction"
69
+ db.transaction( rollback: :always ) do
70
+ example.run
71
+ end
72
+ end
73
+ elsif setting.to_s == 'pending'
74
+ example.metadata[:pending] ||=
75
+ "a configured database URI in #{TESTDB_ENV_VAR}"
76
+ else
77
+ fail "No database connection! " +
78
+ "Ensure you have the #{TESTDB_ENV_VAR} ENV variable set to " +
79
+ "the URI of an (empty) test database you have write permissions to."
80
+ end
81
+ else
82
+ example.run
83
+ end
84
+ end
85
+
86
+ super
87
+ end
88
+
89
+ end # module Thingfish::Metastore::PG::SpecHelpers
90
+
91
+
92
+ ### Mock with RSpec
93
+ RSpec.configure do |c|
94
+ include Thingfish::SpecHelpers
95
+ include Thingfish::SpecHelpers::Constants
96
+
97
+ c.run_all_when_everything_filtered = true
98
+ c.filter_run :focus
99
+ # c.order = 'random'
100
+ c.mock_with( :rspec ) do |mock|
101
+ mock.syntax = :expect
102
+ end
103
+
104
+ c.include( Loggability::SpecHelpers )
105
+ c.include( Thingfish::SpecHelpers )
106
+ c.include( Thingfish::MetastorePGSpecHelpers )
107
+ end
108
+
109
+ # vim: set nosta noet ts=4 sw=4:
110
+
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env rspec -cfd
2
+ #encoding: utf-8
3
+
4
+ require_relative '../../spec_helper'
5
+
6
+ require 'rspec'
7
+
8
+ require 'thingfish/behaviors'
9
+ require 'thingfish/metastore/pg'
10
+
11
+ describe Thingfish::Metastore::PG, db: true do
12
+
13
+ it_should_behave_like "a Thingfish metastore"
14
+
15
+ end
16
+
metadata ADDED
@@ -0,0 +1,247 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: thingfish-metastore-pg
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre20160627113019
5
+ platform: ruby
6
+ authors:
7
+ - Michael Granger
8
+ - Mahlon E. Smith
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain:
12
+ - |
13
+ -----BEGIN CERTIFICATE-----
14
+ MIIEbDCCAtSgAwIBAgIBATANBgkqhkiG9w0BAQsFADA+MQwwCgYDVQQDDANnZWQx
15
+ GTAXBgoJkiaJk/IsZAEZFglGYWVyaWVNVUQxEzARBgoJkiaJk/IsZAEZFgNvcmcw
16
+ HhcNMTYwODIwMTgxNzQyWhcNMTcwODIwMTgxNzQyWjA+MQwwCgYDVQQDDANnZWQx
17
+ GTAXBgoJkiaJk/IsZAEZFglGYWVyaWVNVUQxEzARBgoJkiaJk/IsZAEZFgNvcmcw
18
+ ggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQC/JWGRHO+USzR97vXjkFgt
19
+ 83qeNf2KHkcvrRTSnR64i6um/ziin0I0oX23H7VYrDJC9A/uoUa5nGRJS5Zw/+wW
20
+ ENcvWVZS4iUzi4dsYJGY6yEOsXh2CcF46+QevV8iE+UmbkU75V7Dy1JCaUOyizEt
21
+ TH5UHsOtUU7k9TYARt/TgYZKuaoAMZZd5qyVqhF1vV+7/Qzmp89NGflXf2xYP26a
22
+ 4MAX2qqKX/FKXqmFO+AGsbwYTEds1mksBF3fGsFgsQWxftG8GfZQ9+Cyu2+l1eOw
23
+ cZ+lPcg834G9DrqW2zhqUoLr1MTly4pqxYGb7XoDhoR7dd1kFE2a067+DzWC/ADt
24
+ +QkcqWUm5oh1fN0eqr7NsZlVJDulFgdiiYPQiIN7UNsii4Wc9aZqBoGcYfBeQNPZ
25
+ soo/6za/bWajOKUmDhpqvaiRv9EDpVLzuj53uDoukMMwxCMfgb04+ckQ0t2G7wqc
26
+ /D+K9JW9DDs3Yjgv9k4h7YMhW5gftosd+NkNC/+Y2CkCAwEAAaN1MHMwCQYDVR0T
27
+ BAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFHKN/nkRusdqCJEuq3lgB3fJvyTg
28
+ MBwGA1UdEQQVMBOBEWdlZEBGYWVyaWVNVUQub3JnMBwGA1UdEgQVMBOBEWdlZEBG
29
+ YWVyaWVNVUQub3JnMA0GCSqGSIb3DQEBCwUAA4IBgQAPJzKiT0zBU7kpqe0aS2qb
30
+ FI0PJ4y5I8buU4IZGUD5NEt/N7pZNfOyBxkrZkXhS44Fp+xwBH5ebLbq/WY78Bqd
31
+ db0z6ZgW4LMYMpWFfbXsRbd9TU2f52L8oMAhxOvF7Of5qJMVWuFQ8FPagk2iHrdH
32
+ inYLQagqAF6goWTXgAJCdPd6SNeeSNqA6vlY7CV1Jh5kfNJJ6xu/CVij1GzCLu/5
33
+ DMOr26DBv+qLJRRC/2h34uX71q5QgeOyxvMg+7V3u/Q06DXyQ2VgeeqiwDFFpEH0
34
+ PFkdPO6ZqbTRcLfNH7mFgCBJjsfSjJrn0sPBlYyOXgCoByfZnZyrIMH/UY+lgQqS
35
+ 6Von1VDsfQm0eJh5zYZD64ZF86phSR7mUX3mXItwH04HrZwkWpvgd871DZVR3i1n
36
+ w8aNA5re5+Rt/Vvjxj5AcEnZnZiz5x959NaddQocX32Z1unHw44pzRNUur1GInfW
37
+ p4vpx2kUSFSAGjtCbDGTNV2AH8w9OU4xEmNz8c5lyoA=
38
+ -----END CERTIFICATE-----
39
+ date: 2016-06-27 00:00:00.000000000 Z
40
+ dependencies:
41
+ - !ruby/object:Gem::Dependency
42
+ name: thingfish
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.5'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: loggability
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.10'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.10'
69
+ - !ruby/object:Gem::Dependency
70
+ name: configurability
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.2'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sequel
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '4.35'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '4.35'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pg
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.18'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.18'
111
+ - !ruby/object:Gem::Dependency
112
+ name: hoe-mercurial
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.4'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.4'
125
+ - !ruby/object:Gem::Dependency
126
+ name: hoe-deveiate
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.8'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.8'
139
+ - !ruby/object:Gem::Dependency
140
+ name: hoe-highline
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.2'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.2'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rdoc
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '4.0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '4.0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rspec
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '3.0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '3.0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: hoe
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: '3.15'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: '3.15'
195
+ description: This is a metadata storage plugin for the Thingfish digital asset manager.
196
+ It provides persistent storage for uploaded data to a PostgreSQL table.
197
+ email:
198
+ - ged@FaerieMUD.org
199
+ - mahlon@martini.nu
200
+ executables: []
201
+ extensions: []
202
+ extra_rdoc_files:
203
+ - History.md
204
+ - LICENSE.md
205
+ - Manifest.txt
206
+ - README.md
207
+ files:
208
+ - ".document"
209
+ - ".simplecov"
210
+ - ChangeLog
211
+ - History.md
212
+ - LICENSE.md
213
+ - Manifest.txt
214
+ - README.md
215
+ - Rakefile
216
+ - data/thingfish-metastore-pg/migrations/20150114_initial.rb
217
+ - lib/thingfish/metastore/pg.rb
218
+ - lib/thingfish/metastore/pg/metadata.rb
219
+ - spec/spec_helper.rb
220
+ - spec/thingfish/metastore/pg_spec.rb
221
+ homepage: https://bitbucket.org/ged/thingfish-metastore-pg
222
+ licenses:
223
+ - BSD-3-Clause
224
+ metadata: {}
225
+ post_install_message:
226
+ rdoc_options:
227
+ - "--main"
228
+ - README.md
229
+ require_paths:
230
+ - lib
231
+ required_ruby_version: !ruby/object:Gem::Requirement
232
+ requirements:
233
+ - - ">="
234
+ - !ruby/object:Gem::Version
235
+ version: 2.3.0
236
+ required_rubygems_version: !ruby/object:Gem::Requirement
237
+ requirements:
238
+ - - ">"
239
+ - !ruby/object:Gem::Version
240
+ version: 1.3.1
241
+ requirements: []
242
+ rubyforge_project:
243
+ rubygems_version: 2.5.1
244
+ signing_key:
245
+ specification_version: 4
246
+ summary: This is a metadata storage plugin for the Thingfish digital asset manager
247
+ test_files: []
metadata.gz.sig ADDED
@@ -0,0 +1 @@
1
+ ``�b��qV7Q��µY�Oz�R8B����?Ī'��m���3�6N[�^��I!E�ظR&vk��K���#�I6�<���'�w�R�)od�����A���<3�� �����2�H�M�w}A�x�v$­���نQ�vV.�E��b�Lv�BE1���������9(�������j��pz@�i)�mȴ���c��^BdHUz��Y؜u-@zF[�ңQĩ$}�3�Ɏ�K�F��2P3P��a�8��2�E���y���x4p>�<�;ƘJ�^�^DB~i�K�@�����h�R/�� ��x� @́�M� aD����/`��B���J*ES�8�Svz�i7��g��&ZV���.��Bm�+<�c��\g�=�