epo 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +157 -0
- data/Rakefile +44 -0
- data/TODO +9 -0
- data/lib/epo/core/db.rb +189 -0
- data/lib/epo/core/dispatch_observations.rb +21 -0
- data/lib/epo/core/observer.rb +81 -0
- data/lib/epo.rb +10 -0
- metadata +99 -0
data/README
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
|
2
|
+
= EPO
|
3
|
+
|
4
|
+
EPO is a no-brainer, plain-ruby file system database. If you have objects and
|
5
|
+
need to store them in a clean hierarchy, then EPO is a good choice.
|
6
|
+
EPO is not a good choice if you want to perform optimized queries or if you
|
7
|
+
operate on big datasets.
|
8
|
+
In EPO, each object has a directory, so, it is easy to use programs which write
|
9
|
+
their output at a given file without using temp dirs. Similarly, if you do
|
10
|
+
lots of batch operations on your EPO's resources, the one-directory per
|
11
|
+
resource approach scales well.
|
12
|
+
|
13
|
+
== Philosophy
|
14
|
+
|
15
|
+
EPO only is a small library, so summarizing it's philosophy takes only a few
|
16
|
+
lines:
|
17
|
+
* My filesystem already is a database
|
18
|
+
* My database should be plain ruby and
|
19
|
+
* But my database should be easy to use from non-ruby programs
|
20
|
+
|
21
|
+
== The EPO hierarchy
|
22
|
+
|
23
|
+
EPO tries to build from the lessons of KISS and REST routes.
|
24
|
+
If you're familiar with REST applications, you will find EPO's hierarchy pretty
|
25
|
+
straightforward.
|
26
|
+
|
27
|
+
=== Important Rules
|
28
|
+
|
29
|
+
Here are the simple rules for EPO's databases:
|
30
|
+
* EPO operates on Welo::Resources
|
31
|
+
* each resource has one directory
|
32
|
+
* the directory's path identifies the resource stored
|
33
|
+
* there is one serialization file per (resource, perspective, extension) tuple
|
34
|
+
* any other file in the directory should not impact your ruby application, but may have meaning to other programs (e.g., a thumbnail created by your file-viewer)
|
35
|
+
|
36
|
+
=== Examples
|
37
|
+
|
38
|
+
If you have a resource "user" identified by its "login", if you have three
|
39
|
+
users: peter, jon, and marc.
|
40
|
+
The database directories may look like the following. Between brackets are reference to further explanations.
|
41
|
+
|
42
|
+
$ tree db/
|
43
|
+
db/
|
44
|
+
└── user
|
45
|
+
├── peter
|
46
|
+
| ├── resource-default.json [1a]
|
47
|
+
| └── picture.png [1b]
|
48
|
+
├── jon
|
49
|
+
| ├── resource-default.json [2a]
|
50
|
+
| ├── picture.png [2b]
|
51
|
+
| └── pubkey.rsa.txt [2c]
|
52
|
+
└── marc
|
53
|
+
├── resource-default.json [3a]
|
54
|
+
├── picture.png [3b]
|
55
|
+
└── thumbnail.dat [3c]
|
56
|
+
|
57
|
+
In the previous hierarchy, [1a, 2a, 3a] are the serialization of the user
|
58
|
+
resources under the 'default' (or :default) perspective in the JSON format.
|
59
|
+
The perspective is a concept of Welo's resource, which is basically a list of
|
60
|
+
fields of an object that you want to dump or observe (an administrator may have
|
61
|
+
access to more details than a mere, non-registered user).
|
62
|
+
|
63
|
+
The other objects may or may not be related to your ruby application. A
|
64
|
+
convenient explanation may be: [1b, 2b, 3b] are pictures, most likely your
|
65
|
+
user's headshots, uploaded by your users through your application. User [2b]
|
66
|
+
has given his public key, and we see that your filebrowser has created a
|
67
|
+
thumbnail in [3c], which has nothing to do with your application.
|
68
|
+
|
69
|
+
=== Attention
|
70
|
+
|
71
|
+
The main thing to keep in mind is that filesystems come with a slight problem:
|
72
|
+
file paths are strings.
|
73
|
+
As a result, when designing your EPO hierarchy, you must be aware of:
|
74
|
+
- encoding issues
|
75
|
+
- case sensitivity
|
76
|
+
- ambiguities with path separators
|
77
|
+
|
78
|
+
== Benefits
|
79
|
+
|
80
|
+
Example of things you can do easily with EPO:
|
81
|
+
* use a filesystem explorer to visualize/explore your DB
|
82
|
+
* organize your documents without hassle
|
83
|
+
* use bash scripts for batch processing
|
84
|
+
* use rsync/NFS/git on all or part of your database's content
|
85
|
+
* use ftp/http servers to expose a branch of your DB's hierarchy
|
86
|
+
* have other programs use your DB easily without configuring databases
|
87
|
+
|
88
|
+
Say you have photos to sort. You may sort them by year, by place, by subject or
|
89
|
+
other things. Some software propose you to tag your photos and build a database
|
90
|
+
with these photos for you. Unfortunately, these softwares are often closed, you
|
91
|
+
cannot re-use their database, or it requires lots of effort to script the
|
92
|
+
software to do something it is not ready for (e.g., resize your pictures in a
|
93
|
+
batch). Moreover, the file-viewer of your OS may be good enough to view your
|
94
|
+
pictures. Filesystems have solved many database issues since ages. So, why
|
95
|
+
not just rely on your file system to store your pictures?
|
96
|
+
|
97
|
+
== Usage (please have a look at the examples directory)
|
98
|
+
|
99
|
+
=== EPO is simple
|
100
|
+
There is no complex things in EPO, as an example, there is no:
|
101
|
+
- index
|
102
|
+
- transaction
|
103
|
+
- thread/multiprocess safety
|
104
|
+
- lifecycle hook
|
105
|
+
If you want to add a missing feature, feel free to fork/subclass/add a module,
|
106
|
+
or implement it directly on your filesystem.
|
107
|
+
|
108
|
+
=== Theory
|
109
|
+
|
110
|
+
EPO uses:
|
111
|
+
* Derailleur's paths routing to quickly map paths to resources
|
112
|
+
(we may want to remove this dependency later)
|
113
|
+
* Welo's observers to hook events when iterating on the filesystem
|
114
|
+
|
115
|
+
EPO::DB are just simple, in-memory Ruby objects.
|
116
|
+
They have no connection to take care of, no credentials.
|
117
|
+
|
118
|
+
An EPO::DB may understand several formats (json or yaml are standard choices).
|
119
|
+
You may modify this by changing EPO::DB#extensions .
|
120
|
+
|
121
|
+
EPO::DB are headless, in the sense that they don't store their hierarchy's root
|
122
|
+
in a variable. As a result, an EPO::DB contains the concepts of the models but
|
123
|
+
are not actually tied to the data on the filesystem.
|
124
|
+
You may have two EPO::DB on the same filesystem's root (each understanding
|
125
|
+
different resources or formats).
|
126
|
+
|
127
|
+
=== Commented code bits
|
128
|
+
|
129
|
+
Say you have two models (which are Welo::Resources): Person and Item.
|
130
|
+
Both models must have a "identifying" named :flat_db (this is just a default convention).
|
131
|
+
class Person
|
132
|
+
include Welo::Resource
|
133
|
+
identify :flat_db, [:name]
|
134
|
+
...
|
135
|
+
|
136
|
+
If you want to create a database only able to handle persons
|
137
|
+
EPO::DB.new([Person])
|
138
|
+
|
139
|
+
Creates a database able to handle persons and items
|
140
|
+
db = EPO::DB.new([Person, Item])
|
141
|
+
|
142
|
+
Saves a person with the default perspective, in all format
|
143
|
+
person = Person.new(...)
|
144
|
+
db.save(root, person)
|
145
|
+
|
146
|
+
Iterates on all the DB items (as observations)
|
147
|
+
db.each_resource(root) { |observation| ... }
|
148
|
+
|
149
|
+
== Dependencies
|
150
|
+
|
151
|
+
welo >= 0.1.0
|
152
|
+
derailleur >= 0.5.0
|
153
|
+
a JSON library if you use json formatting
|
154
|
+
|
155
|
+
== License
|
156
|
+
|
157
|
+
The MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
|
2
|
+
require 'rubygems'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
|
5
|
+
$LOAD_PATH.unshift('lib')
|
6
|
+
require 'epo'
|
7
|
+
|
8
|
+
spec = Gem::Specification.new do |s|
|
9
|
+
|
10
|
+
s.name = 'epo'
|
11
|
+
s.rubyforge_project = 'epo'
|
12
|
+
s.version = EPO::VERSION
|
13
|
+
s.author = EPO::AUTHORS.first
|
14
|
+
s.homepage = EPO::WEBSITE
|
15
|
+
s.summary = "A no-brainer, plain-ruby database"
|
16
|
+
s.email = "crapooze@gmail.com"
|
17
|
+
s.platform = Gem::Platform::RUBY
|
18
|
+
|
19
|
+
s.files = [
|
20
|
+
'Rakefile',
|
21
|
+
'TODO',
|
22
|
+
'README',
|
23
|
+
'lib/epo.rb',
|
24
|
+
'lib/epo/core/db.rb',
|
25
|
+
'lib/epo/core/observer.rb',
|
26
|
+
'lib/epo/core/dispatch_observations.rb',
|
27
|
+
]
|
28
|
+
|
29
|
+
s.require_path = 'lib'
|
30
|
+
s.bindir = 'bin'
|
31
|
+
s.executables = []
|
32
|
+
s.has_rdoc = true
|
33
|
+
|
34
|
+
s.add_dependency('derailleur', '>= 0.0.5')
|
35
|
+
s.add_dependency('welo', '>= 0.1.0')
|
36
|
+
end
|
37
|
+
|
38
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
39
|
+
pkg.need_tar = true
|
40
|
+
end
|
41
|
+
|
42
|
+
task :gem => ["pkg/#{spec.name}-#{spec.version}.gem"] do
|
43
|
+
puts "generated #{spec.version}"
|
44
|
+
end
|
data/TODO
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
* delete record/observation
|
2
|
+
* maybe with some modifications to Derailleur:
|
3
|
+
- graceful not found case
|
4
|
+
- clever pruning when finding:
|
5
|
+
- when path is a directory and there is no node, prune
|
6
|
+
- when path is a directory but there is a node, continue
|
7
|
+
- cache observation structs somewhere in the db or in the observer
|
8
|
+
* fiber-friendly batch operations
|
9
|
+
* register on inotify events
|
data/lib/epo/core/db.rb
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
|
2
|
+
require 'derailleur/core/application'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module EPO
|
6
|
+
class DB
|
7
|
+
include Derailleur::Application
|
8
|
+
|
9
|
+
# The list of understood models, models must behave like Welo::Resources
|
10
|
+
attr_reader :models
|
11
|
+
|
12
|
+
# A list of understood extensions (e.g., '.json', '.yaml'), default is '.json'
|
13
|
+
attr_reader :extensions
|
14
|
+
|
15
|
+
# The name of the identifiers to use to map Welo::Resources to directories
|
16
|
+
attr_reader :identifying_sym
|
17
|
+
|
18
|
+
# Creates a new DB
|
19
|
+
# models: the list of Welo::Resource models to use (i.e., classes)
|
20
|
+
def initialize(models, params={})
|
21
|
+
@models = models
|
22
|
+
@extensions = params[:extensions] || ['.json']
|
23
|
+
@identifying_sym = params[:identifying_sym] || :flat_db
|
24
|
+
build!
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# creates the tree to map the DB structure to classes paths
|
30
|
+
# does not register the model because it uses existing one
|
31
|
+
def build!
|
32
|
+
models.each do |model|
|
33
|
+
build_route_for_model(model)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
public
|
38
|
+
|
39
|
+
# Registers a model on a derailleur node at its path_model
|
40
|
+
# Does not modify self.models
|
41
|
+
def build_route_for_model(model)
|
42
|
+
path = model.path_model(identifying_sym) #XXX allow to change the db identifying name
|
43
|
+
node = build_route(path)
|
44
|
+
node.content = model
|
45
|
+
end
|
46
|
+
|
47
|
+
# Updates self.models and build a route for the model
|
48
|
+
# raise an error if the model is already known
|
49
|
+
def register_model(model)
|
50
|
+
raise ArgumentError, "already know this model" if models.include?(model)
|
51
|
+
models << model
|
52
|
+
build_route_for_model(model)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Loads the file at path and turn it to a ruby object based on the file extension.
|
56
|
+
# If the file extension is not supported, will raise an error.
|
57
|
+
# uses JSON.parse(File.read(path)) and YAML.load_file(path) for '.json' and '.yaml'
|
58
|
+
# otherwise, will forward the handling such that
|
59
|
+
# '.foobaar' maps to :read_foobar(path)
|
60
|
+
def read_file(path)
|
61
|
+
raise ArgumentError, "don't know extension #{ext}, use from [#{extensions.join(', ')}]" unless understands_ext?(path)
|
62
|
+
ext = File.extname(path)
|
63
|
+
case ext
|
64
|
+
when '.json'
|
65
|
+
JSON.parse(File.read(path))
|
66
|
+
when '.yaml'
|
67
|
+
YAML.load_file(path)
|
68
|
+
else
|
69
|
+
self.send "read_#{ext.tr('.','')}", path
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Wether or not this DB understands the extension of the file at path.
|
74
|
+
def understands_ext?(path)
|
75
|
+
extensions.find{|ext| ext == File.extname(path)}
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns the perspective name and extension name (a 2 items array)
|
79
|
+
# for the given path.
|
80
|
+
# By default the blobs for resources content are stored in files named
|
81
|
+
# 'resource-<persp><ext>' with ext starting with a dot
|
82
|
+
def persp_and_ext_for_basename(path)
|
83
|
+
base = File.basename(path).sub('resource-','').sub(/\.[^\.]+$/,'')
|
84
|
+
[base, File.extname(path)]
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns the basename of a resource blob for a perspective named persp and
|
88
|
+
# in a format with extension ext (including the leading dot).
|
89
|
+
# see also persp_and_ext_for_basename
|
90
|
+
def basename_for_persp_and_ext(persp, ext)
|
91
|
+
"resource-#{persp}#{ext}"
|
92
|
+
end
|
93
|
+
|
94
|
+
# Saves one or more resource at the filesystem path given at root
|
95
|
+
# This method is mainly an handy helper around batch_save, look at the
|
96
|
+
# source and at batch_save's doc
|
97
|
+
def save(root, resource, perspective=:default, exts=nil)
|
98
|
+
exts ||= extensions
|
99
|
+
batch_save(root, [resource].flatten, [perspective].flatten, [exts].flatten)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Saves all the resources, under all the perspectives persps, and all format
|
103
|
+
# given by extensions exts at the filesystem path root.
|
104
|
+
# resources, perps, exts, must respond to :each, like for Enumerable
|
105
|
+
def batch_save(root, resources, persps, exts)
|
106
|
+
batch_save_actions(root, resources, persps, exts) do |action|
|
107
|
+
action.perform
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Yields all the action needed to store all the resources
|
112
|
+
# at perspectives persps and with formats exts.
|
113
|
+
# All actions respond to :perform (it is when they're executed).
|
114
|
+
# If no block given, returns an Enumerator with these actions.
|
115
|
+
def batch_save_actions(root, resources, persps, exts)
|
116
|
+
if block_given?
|
117
|
+
resources.each do |resource|
|
118
|
+
db_path = File.join(root, resource.path(identifying_sym))
|
119
|
+
yield PrepareDirAction.new(db_path)
|
120
|
+
exts.each do |ext|
|
121
|
+
persps.each do |persp|
|
122
|
+
basename = basename_for_persp_and_ext(persp, ext)
|
123
|
+
resource_path = File.join(db_path, basename)
|
124
|
+
yield StoreResourceAction.new(resource_path, resource, persp, ext)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
else
|
129
|
+
Enumerator.new(self, root, resources, persps, exts)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# An action to prepare the directory for a resource
|
134
|
+
PrepareDirAction = Struct.new(:path) do
|
135
|
+
def perform
|
136
|
+
FileUtils.mkdir_p(path)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# An action to serialize and store a resource seen in a given perspective
|
141
|
+
# into a file at path, under the format with a format given by its
|
142
|
+
# extension name.
|
143
|
+
StoreResourceAction = Struct.new(:path, :resource, :persp, :ext) do
|
144
|
+
def perform
|
145
|
+
data = resource.to_ext(ext.to_s, persp)
|
146
|
+
store_data_at_path(data, path)
|
147
|
+
end
|
148
|
+
|
149
|
+
def store_data_at_path(data, path, enum=:each, mode='w')
|
150
|
+
File.open(path, mode) do |f|
|
151
|
+
write_data_to_io(data, f, enum)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def write_data_to_io(data, io, enum=:each)
|
156
|
+
if data.respond_to?(enum)
|
157
|
+
data.each do |buf|
|
158
|
+
io << buf
|
159
|
+
end
|
160
|
+
else
|
161
|
+
io << data
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Returns a new EPO::Observer for itself
|
167
|
+
def observer
|
168
|
+
Observer.for_db(self)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Iterates on every resource found and understood in the filesystem
|
172
|
+
# directory root.
|
173
|
+
# If no block is given, returns an iterator.
|
174
|
+
def each_resource(root)
|
175
|
+
if block_given?
|
176
|
+
xp = observer
|
177
|
+
models.each do |model|
|
178
|
+
xp.on(model) do |obs|
|
179
|
+
yield obs
|
180
|
+
end
|
181
|
+
end
|
182
|
+
xp.read_tree(root)
|
183
|
+
else
|
184
|
+
Enumerator.new(self, :each_resource, root)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
module EPO
|
3
|
+
module DispatchObservations
|
4
|
+
# Register an event when an observation of a resource
|
5
|
+
# matching a given model is made
|
6
|
+
def on(model,&blk)
|
7
|
+
register(model,&blk)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
# Call each blocks for an observation registering from the
|
13
|
+
# observation's resource model
|
14
|
+
def dispatch(observation)
|
15
|
+
blks = registrations[observation.class.resource] || []
|
16
|
+
blks.each do |blk|
|
17
|
+
blk.call(observation)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
|
2
|
+
require 'welo'
|
3
|
+
require 'find'
|
4
|
+
require 'epo/core/db'
|
5
|
+
require 'epo/core/dispatch_observations'
|
6
|
+
|
7
|
+
module EPO
|
8
|
+
# An Observer is a object able to look at the filesystem and
|
9
|
+
# observe resources in it.
|
10
|
+
class Observer < Welo::Observer
|
11
|
+
include DispatchObservations
|
12
|
+
# in EPO, the source of an observation is:
|
13
|
+
# - a db object able to make a ruby object from a file path
|
14
|
+
# - a file path
|
15
|
+
Source = Struct.new(:db, :path) do
|
16
|
+
def observe
|
17
|
+
db.read_file(path)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# The DB able to tell if a path is understandable or not
|
22
|
+
attr_accessor :db
|
23
|
+
|
24
|
+
# Creates and return a new observer from a DB.
|
25
|
+
# It will take the models from the DB.
|
26
|
+
def self.for_db(db)
|
27
|
+
obj = self.new(db.models)
|
28
|
+
obj.db = db
|
29
|
+
obj
|
30
|
+
end
|
31
|
+
|
32
|
+
# Creates a new observer for given models.
|
33
|
+
# Will instanciate a new DB.
|
34
|
+
def initialize(models=[])
|
35
|
+
super(models)
|
36
|
+
@db = DB.new(models)
|
37
|
+
register(:observation) do |o|
|
38
|
+
dispatch(o)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Read a single path, the root is the part of the path corresponding
|
43
|
+
# to where on the filesystem the database is rooted.
|
44
|
+
# e.g. for a path in a photo collection:
|
45
|
+
# /home/crapooze/project/foobar/db/photo/1
|
46
|
+
# the root is likely to be:
|
47
|
+
# /home/crapooze/project/foobar/db
|
48
|
+
# If a successful observation happens, then it will call the relevant hooks.
|
49
|
+
def read_path(path, root=nil)
|
50
|
+
full_dirname, base = File.split(path)
|
51
|
+
dirname = if root
|
52
|
+
full_dirname.sub(root,'')
|
53
|
+
else
|
54
|
+
full_dirname
|
55
|
+
end
|
56
|
+
#XXX may raise an exception for unknown path, we should rescue this/use a
|
57
|
+
#silent method and test for nil
|
58
|
+
node = db.get_route(dirname)
|
59
|
+
|
60
|
+
persp_str = db.persp_and_ext_for_basename(path).first
|
61
|
+
persp = node.content.perspectives.keys.find{|k| k.to_s == persp_str}
|
62
|
+
#XXX no need to create this many time, should cache it
|
63
|
+
st = Welo::ObservationStruct.new_for_resource_in_perspective(node.content, persp)
|
64
|
+
source = Source.new(db, path)
|
65
|
+
observe_source(source, st)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Recursively reads the files in the filesystem (with Find).
|
69
|
+
# For each path, will try to read_path
|
70
|
+
# Currently, there is no pruning, or control possible.
|
71
|
+
def read_tree(root)
|
72
|
+
Find.find(root) do |path|
|
73
|
+
if File.directory?(path)
|
74
|
+
#XXX maybe prune the branch if valid but has no content
|
75
|
+
elsif db.understands_ext?(path)
|
76
|
+
read_path(path, root)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/epo.rb
ADDED
metadata
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: epo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- crapooze
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-03-19 00:00:00 -04:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: derailleur
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
- 0
|
31
|
+
- 5
|
32
|
+
version: 0.0.5
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: welo
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
- 1
|
46
|
+
- 0
|
47
|
+
version: 0.1.0
|
48
|
+
type: :runtime
|
49
|
+
version_requirements: *id002
|
50
|
+
description:
|
51
|
+
email: crapooze@gmail.com
|
52
|
+
executables: []
|
53
|
+
|
54
|
+
extensions: []
|
55
|
+
|
56
|
+
extra_rdoc_files: []
|
57
|
+
|
58
|
+
files:
|
59
|
+
- Rakefile
|
60
|
+
- TODO
|
61
|
+
- README
|
62
|
+
- lib/epo.rb
|
63
|
+
- lib/epo/core/db.rb
|
64
|
+
- lib/epo/core/observer.rb
|
65
|
+
- lib/epo/core/dispatch_observations.rb
|
66
|
+
has_rdoc: true
|
67
|
+
homepage: https://github.com/crapooze/epo
|
68
|
+
licenses: []
|
69
|
+
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
none: false
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
segments:
|
81
|
+
- 0
|
82
|
+
version: "0"
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
segments:
|
89
|
+
- 0
|
90
|
+
version: "0"
|
91
|
+
requirements: []
|
92
|
+
|
93
|
+
rubyforge_project: epo
|
94
|
+
rubygems_version: 1.3.7
|
95
|
+
signing_key:
|
96
|
+
specification_version: 3
|
97
|
+
summary: A no-brainer, plain-ruby database
|
98
|
+
test_files: []
|
99
|
+
|