activefile 0.0.0beta
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +35 -0
- data/Rakefile +18 -0
- data/lib/active_file/adapter.rb +273 -0
- data/lib/active_file/base.rb +21 -0
- data/lib/activefile.rb +29 -0
- metadata +49 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NTQ1YjFmMGQ2NjI5ZWU4Y2ZjYTNhYjY2NjQwYTE1ZDcxZmI4ODM4ZQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
NGZhMzQwMDY4NzVmNjMxZTY2NjFmYzhjMjliNWU5OTQwMjQ1ZjhjOQ==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
YTFhZjEyNDZiYWE4ZTAwNTE0MGQzZWQ5NjQ0NzNmMTAwMWQ0MjYwMWM2MTRm
|
10
|
+
ZmU3ZThiODUzNzY0Yzc1N2U1YmM1YjQ4YjA2YjI2ODg5ZTE0OTkyMWQ2NWYz
|
11
|
+
MjMwZmI3ZDBlNTAyOTFhNjE4NjczZThkM2VmZDc3MjA4ZjkyNGQ=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
OTU4ZmZjN2RmMGJmZTUxYjM1OTk4YmVjMDczNDQ5NzFkYjQ3MTEwNGY5OGQ2
|
14
|
+
MmYzNWMwNzFlNjcwZTljZDE0OTliMmMzZjM2OTI1MTJlNDczZGJmN2Q5YWIy
|
15
|
+
OTNjZWIxZDU2ODk5ZWRlNzY0NjI0ZGVjM2MyNjM3NTdmMjJhZGY=
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
== Gem under construction. Base functionality already done, but I still need reorganize it. I work with Unit tests now.
|
2
|
+
|
3
|
+
Please, be patient! All work will be done untill May.
|
4
|
+
|
5
|
+
== Welcome to ActiveFile
|
6
|
+
|
7
|
+
ActiveFile is a lightweight file system ORM.
|
8
|
+
|
9
|
+
Build a persistent domain model by mapping file system objects to Ruby classes. It inherits ActiveRecord-similar interface.
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
class Shop < ActiveFile::Base
|
14
|
+
parent_to :product
|
15
|
+
end
|
16
|
+
|
17
|
+
class Product < ActiveFile::Base
|
18
|
+
child_of :shop
|
19
|
+
end
|
20
|
+
|
21
|
+
shop = Shop.new(:name => "Apple Store")
|
22
|
+
shop.save!
|
23
|
+
|
24
|
+
Shop.all.size #> 1
|
25
|
+
|
26
|
+
iPad = Product.new(:name => "iPad", :parent => shop, :data => "The iPad is a line of tablet computers designed and marketed by Apple Inc., which runs Apple's iOS operating system.")
|
27
|
+
iPad.save!
|
28
|
+
|
29
|
+
product = Product.where(:name => "iPad")[0]
|
30
|
+
product.data #> "The iPad "...
|
31
|
+
product.shop #> <Shop instance>
|
32
|
+
product.shop.name #> "Apple Store"
|
33
|
+
|
34
|
+
|
35
|
+
# In result, two persistent files were created, accessible via ORM mechanism.
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
desc "Default Task"
|
3
|
+
task default: [ :test ]
|
4
|
+
|
5
|
+
# Run the unit tests
|
6
|
+
#Rake::TestTask.new { |t|
|
7
|
+
## t.libs << "test"
|
8
|
+
# t.pattern = 'test/**/*_test.rb'
|
9
|
+
# t.warning = true
|
10
|
+
# t.verbose = true
|
11
|
+
#}
|
12
|
+
|
13
|
+
task :test do
|
14
|
+
ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME'))
|
15
|
+
Dir.glob("test/**/*_test.rb").all? do |file|
|
16
|
+
sh(ruby, '-Ilib:test', file)
|
17
|
+
end or raise "Failures"
|
18
|
+
end
|
@@ -0,0 +1,273 @@
|
|
1
|
+
module ActiveFile
|
2
|
+
# Data Source Storage Adapter
|
3
|
+
module Adapter
|
4
|
+
require 'fileutils'
|
5
|
+
RAISE_TRUE = true
|
6
|
+
RAISE_FALSE = false
|
7
|
+
|
8
|
+
def initialize args
|
9
|
+
super args
|
10
|
+
end
|
11
|
+
|
12
|
+
def base_folder arg
|
13
|
+
puts "BaseFolder is #{arg}"
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
# touch file to read!
|
18
|
+
def load!
|
19
|
+
self.data
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def data= data_arg
|
24
|
+
@self_data = data_arg
|
25
|
+
end
|
26
|
+
|
27
|
+
def data
|
28
|
+
if @self_data == nil
|
29
|
+
@self_data = File.read(self.get_source_path)
|
30
|
+
end
|
31
|
+
@self_data || ""
|
32
|
+
end
|
33
|
+
|
34
|
+
# Main rule to link attaches (css) with their targets (layouts, contents). Special filename format used for this purpose;
|
35
|
+
def attach_my_name!
|
36
|
+
# Is we an attached instance?
|
37
|
+
unless self.target.nil?
|
38
|
+
#@ext = CSS_EXT if self.type == SourceType::CSS
|
39
|
+
self.name = target.type.to_s + TARGET_DIVIDER + target.name #+ @ext.to_s
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Get source folder. Create, if not exists.
|
44
|
+
def get_source_folder
|
45
|
+
Adapter.get_source_folder(type)
|
46
|
+
end
|
47
|
+
def get_name
|
48
|
+
attach_my_name!
|
49
|
+
return name
|
50
|
+
end
|
51
|
+
# Get source filename
|
52
|
+
def get_filename
|
53
|
+
get_name + get_extension
|
54
|
+
end
|
55
|
+
# Alias for get_source_path
|
56
|
+
def get_filepath
|
57
|
+
get_source_path
|
58
|
+
end
|
59
|
+
# Get source path. For targeted objects, target name + '--' + target type appends
|
60
|
+
def get_source_path
|
61
|
+
raise ArgumentError, 'Name can not be blank!' if name.blank? && target.nil?
|
62
|
+
raise ArgumentError, 'Target name can not be blank!' if target && target.name.blank?
|
63
|
+
get_source_folder + get_filename
|
64
|
+
end
|
65
|
+
def get_extension
|
66
|
+
return ".scss" if type == SourceType::CSS
|
67
|
+
return extension.blank? ? "" : "."+extension
|
68
|
+
|
69
|
+
type_ext = SOURCE_TYPE_EXTENSIONS[type.to_i] || ""
|
70
|
+
return "" if type_ext == "*"
|
71
|
+
if type_ext == "*"
|
72
|
+
unless new_record?
|
73
|
+
# Custom extension, from filename
|
74
|
+
Dir.glob(dir+"*").each do |f|
|
75
|
+
name_with_extension = f.split('/').last
|
76
|
+
name, ext = name_with_extension.split('.')
|
77
|
+
if name == get_name
|
78
|
+
type_ext = ext
|
79
|
+
break
|
80
|
+
end
|
81
|
+
end
|
82
|
+
else
|
83
|
+
_name, _ext = name.split(".")
|
84
|
+
unless _name.empty? && _ext.empty?
|
85
|
+
type_ext = _ext
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
type_ext = "." + type_ext unless type_ext.empty?
|
90
|
+
type_ext
|
91
|
+
end
|
92
|
+
# Get unique id of the source
|
93
|
+
def get_id
|
94
|
+
ID_PREFIX + type.to_s + ID_DIVIDER + get_name
|
95
|
+
end
|
96
|
+
# Rename the source file, return boolean operation result
|
97
|
+
def rename(new_file_name)
|
98
|
+
raise "Source is new record, call the save! method before rename." if new_record?
|
99
|
+
# rename attached file, if present ()
|
100
|
+
attach = get_attach
|
101
|
+
# later..
|
102
|
+
old_file_path = get_source_path
|
103
|
+
new_file_path = get_source_folder + new_file_name + get_extension
|
104
|
+
b_result = !!File.rename(old_file_path, new_file_path)
|
105
|
+
raise "Unable to rename source" unless b_result
|
106
|
+
self.name = new_file_name
|
107
|
+
if attach
|
108
|
+
# to rename attach, create new copy of attached object with new target name, and delete old
|
109
|
+
attach_source = attach.clone
|
110
|
+
attach_source.target = self
|
111
|
+
b_result = attach_source.save! && attach.delete!
|
112
|
+
raise 'Attached file rename failed' unless b_result
|
113
|
+
end
|
114
|
+
b_result
|
115
|
+
end
|
116
|
+
def new_record?
|
117
|
+
p get_source_path
|
118
|
+
!File.exists?(get_source_path)
|
119
|
+
end
|
120
|
+
private
|
121
|
+
def save_method(raise_exception_on_error)
|
122
|
+
File.open(get_source_path, "w") do
|
123
|
+
|file| file.write(data.force_encoding('utf-8'))
|
124
|
+
end
|
125
|
+
rescue => e
|
126
|
+
return raise_exception_on_error == RAISE_TRUE ? raise(e) : false
|
127
|
+
end
|
128
|
+
def delete_method(raise_exception_on_error)
|
129
|
+
delete_file_name = get_source_path
|
130
|
+
File.delete(delete_file_name)
|
131
|
+
return true
|
132
|
+
rescue => e
|
133
|
+
return raise_exception_on_error == RAISE_TRUE ? raise(e) : false
|
134
|
+
end
|
135
|
+
public
|
136
|
+
# Save the source, return boolean operation result
|
137
|
+
def save
|
138
|
+
save_method(RAISE_FALSE)
|
139
|
+
end
|
140
|
+
def save!
|
141
|
+
save_method(RAISE_TRUE)
|
142
|
+
end
|
143
|
+
# Delete the source: if successful returns true, else return false
|
144
|
+
def delete
|
145
|
+
delete_method(RAISE_FALSE)
|
146
|
+
end
|
147
|
+
# Delete the source: if successful returns true, else raise an error
|
148
|
+
def delete!
|
149
|
+
raise StandardError, "Unable to delete file if object hasn't been saved yet" if new_record?
|
150
|
+
delete_method(RAISE_TRUE)
|
151
|
+
end
|
152
|
+
def get_target_type
|
153
|
+
name.split(TARGET_DIVIDER)[0]
|
154
|
+
end
|
155
|
+
def get_target_name
|
156
|
+
name.split(TARGET_DIVIDER)[1]
|
157
|
+
end
|
158
|
+
# Get target object
|
159
|
+
def get_target
|
160
|
+
return nil unless name.include?(TARGET_DIVIDER)
|
161
|
+
#target_type, target_name = name[0..-2].split(TARGET_DIVIDER)
|
162
|
+
target_type = get_target_type
|
163
|
+
target_name = get_target_name
|
164
|
+
|
165
|
+
target_type_extension = SOURCE_TYPE_EXTENSIONS[target_type.to_i]
|
166
|
+
unless target_type_extension.empty?
|
167
|
+
target_type_extension = "." + target_type_extension
|
168
|
+
end
|
169
|
+
|
170
|
+
target = Adapter.get_source_folder(target_type) + target_name + target_type_extension
|
171
|
+
return nil unless File.exists?(target)
|
172
|
+
return Source.new({ :type => target_type.to_i, :name => target_name, :data => nil })
|
173
|
+
end
|
174
|
+
# Get attached object
|
175
|
+
def get_attach attach_type=SourceType::CSS
|
176
|
+
Adapter.where(:type => attach_type, :name => get_attached_name).first
|
177
|
+
end
|
178
|
+
def get_attached_name
|
179
|
+
type.to_s + TARGET_DIVIDER + name
|
180
|
+
end
|
181
|
+
#def get_attached_filename
|
182
|
+
# attached_file_extension = SOURCE_TYPE_EXTENSIONS[attach_type.to_i]
|
183
|
+
# attached_file_extension = "." + attached_file_extension unless attached_file_extension.empty?
|
184
|
+
# type.to_s + TARGET_DIVIDER + name + attached_file_extension
|
185
|
+
#end
|
186
|
+
def get_attach_or_create attach_type=SourceType::CSS
|
187
|
+
attach = get_attach(attach_type)
|
188
|
+
if attach.nil?
|
189
|
+
attach = Source.new(:type => attach_type, :extension => SOURCE_TYPE_EXTENSIONS[attach_type])
|
190
|
+
attach.target = self
|
191
|
+
attach.save!
|
192
|
+
end
|
193
|
+
attach
|
194
|
+
end
|
195
|
+
#
|
196
|
+
# STATIC METHODS MODULE
|
197
|
+
module ClassMethods
|
198
|
+
# Creates a new source instance and saves it to disk. Returns the newly created source. If a failure has occurred or source already exists -
|
199
|
+
# an exception will be raised.
|
200
|
+
def create(attributes={})
|
201
|
+
raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
|
202
|
+
source_instance = Source.new(attributes)
|
203
|
+
raise 'Source file with such name already exists!' if File.exists?(source_instance.get_source_path)
|
204
|
+
source_instance.save!
|
205
|
+
return source_instance
|
206
|
+
end
|
207
|
+
# Get source folder for any source type. Create, if not exists.
|
208
|
+
def get_source_folder(type)
|
209
|
+
source_folder = Rails.env == 'test' ? TEST_SOURCE_FOLDERS[type.to_i || SourceType::UNDEFINED] : SOURCE_FOLDERS[type.to_i || SourceType::UNDEFINED]
|
210
|
+
FileUtils.mkpath(source_folder) unless File.exists?(source_folder)
|
211
|
+
return source_folder
|
212
|
+
end
|
213
|
+
# Get names array of all sources with specified type
|
214
|
+
def all_by_type(source_type)
|
215
|
+
files = Array.new
|
216
|
+
dir = Adapter.get_source_folder(source_type)
|
217
|
+
source_extension = SOURCE_TYPE_EXTENSIONS[source_type.to_i]
|
218
|
+
|
219
|
+
Dir.glob(dir+"*").each do |f|
|
220
|
+
name_with_extension = f.split('/').last
|
221
|
+
extension = name_with_extension.split('.').size > 1 ? name_with_extension.split('.').last : ""
|
222
|
+
name_without_extension = nil
|
223
|
+
|
224
|
+
name_without_extension = name_with_extension
|
225
|
+
|
226
|
+
if source_extension.blank?
|
227
|
+
name_without_extension = name_with_extension
|
228
|
+
else
|
229
|
+
name_without_extension = source_extension == "*" ? name_with_extension.split('.').first : name_with_extension[0..-source_extension.length-2]
|
230
|
+
end
|
231
|
+
|
232
|
+
s = Source.new({ :type => source_type, :name => name_without_extension, :extension => extension, :data => nil })
|
233
|
+
target_object = s.get_target
|
234
|
+
s.target = target_object unless target_object.nil?
|
235
|
+
files.push(s)
|
236
|
+
end
|
237
|
+
return files
|
238
|
+
end
|
239
|
+
def all
|
240
|
+
(Rails.env == 'test' ? TEST_SOURCE_FOLDERS : SOURCE_FOLDERS).map {|key_type, val| Adapter.all_by_type(key_type) }.reject { |ar| ar.empty? }.flatten
|
241
|
+
end
|
242
|
+
# Find the source
|
243
|
+
def where(attributes)
|
244
|
+
raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
|
245
|
+
Adapter.all.select do |source|
|
246
|
+
match = true
|
247
|
+
attributes.each{|key, val|
|
248
|
+
match = false if source.send(key) != val
|
249
|
+
}
|
250
|
+
match
|
251
|
+
end
|
252
|
+
end
|
253
|
+
def find_by_id(id)
|
254
|
+
id = id[ID_PREFIX.size .. -1]
|
255
|
+
type, name = id.include?(TARGET_DIVIDER) ? (id).split(ID_DIVIDER) : id.split(ID_DIVIDER)
|
256
|
+
Adapter.find_by_name_and_type(name, type.to_i).first
|
257
|
+
end
|
258
|
+
# Complex finders:
|
259
|
+
def method_missing(m, *args, &block)
|
260
|
+
if m.to_s.index("find_by_") == 0
|
261
|
+
attributes = m["find_by_".size..-1].split("_and_")
|
262
|
+
raise "Attributes count expected: #{attributes.size}, got: #{args.size}" unless attributes.size == args.size
|
263
|
+
match_hash = {}
|
264
|
+
attributes.each_with_index {|attr, index| match_hash[attr.to_sym] = args[index]}
|
265
|
+
return Adapter.where(match_hash)
|
266
|
+
else
|
267
|
+
puts "There's no method called #{m} here -- please try again with args #{args}"
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
extend ClassMethods
|
272
|
+
end
|
273
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ActiveFile
|
2
|
+
require 'ostruct'
|
3
|
+
class Base < OpenStruct
|
4
|
+
|
5
|
+
|
6
|
+
include Adapter
|
7
|
+
extend Adapter::ClassMethods
|
8
|
+
def ahola
|
9
|
+
puts 'ahols here!'
|
10
|
+
end
|
11
|
+
#'a'.camelize.safe_constantize
|
12
|
+
#def child_of(parent_name)
|
13
|
+
# puts "ok, I am a child of #{parent_name}"
|
14
|
+
#end
|
15
|
+
#
|
16
|
+
#def parent_to(child_name)
|
17
|
+
# puts "OH, I am a parent to #{child_name}"
|
18
|
+
#end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
data/lib/activefile.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2013 ariekdev
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
require "active_support/dependencies/autoload"
|
25
|
+
module ActiveFile
|
26
|
+
extend ActiveSupport::Autoload
|
27
|
+
autoload :Base
|
28
|
+
autoload :Adapter
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activefile
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0beta
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vitaly Pestov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-03-16 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Build a hierarchical model of filesystem objects.
|
14
|
+
email: vitalyp@softwareplanet.uk.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/activefile.rb
|
20
|
+
- lib/active_file/adapter.rb
|
21
|
+
- lib/active_file/base.rb
|
22
|
+
- MIT-LICENSE
|
23
|
+
- Rakefile
|
24
|
+
- README.rdoc
|
25
|
+
homepage: http://www.interlink-ua.com
|
26
|
+
licenses:
|
27
|
+
- MIT
|
28
|
+
metadata: {}
|
29
|
+
post_install_message:
|
30
|
+
rdoc_options: []
|
31
|
+
require_paths:
|
32
|
+
- lib
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.9.3
|
38
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ! '>'
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 1.3.1
|
43
|
+
requirements: []
|
44
|
+
rubyforge_project:
|
45
|
+
rubygems_version: 2.0.2
|
46
|
+
signing_key:
|
47
|
+
specification_version: 4
|
48
|
+
summary: Object-relational mapper framework. Please, be patient. Under construction.
|
49
|
+
test_files: []
|