datapackage 0.0.4 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE.md +4 -2
- data/README.md +82 -143
- data/lib/datapackage/exceptions.rb +12 -0
- data/lib/datapackage/package.rb +177 -156
- data/lib/datapackage/registry.rb +81 -0
- data/lib/datapackage/resource.rb +79 -0
- data/lib/datapackage/schema.rb +111 -0
- data/lib/datapackage/version.rb +1 -1
- data/lib/datapackage.rb +7 -2
- metadata +131 -31
- data/etc/README.md +0 -18
- data/etc/csvddf-dialect-schema.json +0 -24
- data/etc/datapackage-schema.json +0 -208
- data/etc/jsontable-schema.json +0 -34
- data/lib/datapackage/validator.rb +0 -229
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0dbfa361366c6830bb4624347821cc07c754d903
|
4
|
+
data.tar.gz: 4fc8b53abca06e6885d686cde540a21ad591aa49
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 53d75e66e0b49d2a92350d826f8e33e371f76b162fd8c94ecccf566064aa3e0e5c6f98aa2ec89deafa7126a359d804383df03163e91035721f7f4c22efcbcda9
|
7
|
+
data.tar.gz: 4e4b96bef09d7e46f06e7eac048ebacf45aebf3bcb50a6b9f44bf1ff578e7153fc49e5cb64239b76d84a6c936128c5e8394d2f50c67de25203565f0d997cc7df
|
data/LICENSE.md
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
Copyright
|
1
|
+
##Copyright (c) 2016 The Open Data Institute
|
2
|
+
|
3
|
+
#MIT License
|
2
4
|
|
3
5
|
Permission is hereby granted, free of charge, to any person obtaining
|
4
6
|
a copy of this software and associated documentation files (the
|
@@ -17,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
19
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
20
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
21
|
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.
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,11 +1,14 @@
|
|
1
|
+
[](https://travis-ci.org/theodi/datapackage.rb)
|
2
|
+
[](https://gemnasium.com/theodi/datapackage.rb)
|
3
|
+
[](https://coveralls.io/r/theodi/datapackage.rb)
|
4
|
+
[](https://codeclimate.com/github/theodi/datapackage.rb)
|
5
|
+
[](https://rubygems.org/gems/datapackage)
|
6
|
+
[](http://theodi.mit-license.org)
|
7
|
+
|
1
8
|
# DataPackage.rb
|
2
9
|
|
3
10
|
A ruby library for working with [Data Packages](http://dataprotocols.org/data-packages/).
|
4
11
|
|
5
|
-
[](http://jenkins.theodi.org/job/datapackage.rb-master/)
|
6
|
-
[](https://codeclimate.com/github/theodi/datapackage.rb)
|
7
|
-
[](https://gemnasium.com/theodi/datapackage.rb)
|
8
|
-
|
9
12
|
The library is intending to support:
|
10
13
|
|
11
14
|
* Parsing and using data package metadata and data
|
@@ -15,179 +18,115 @@ The library is intending to support:
|
|
15
18
|
|
16
19
|
Add the gem into your Gemfile:
|
17
20
|
|
18
|
-
|
21
|
+
```
|
22
|
+
gem 'datapackage.rb'
|
23
|
+
```
|
19
24
|
|
20
25
|
Or:
|
21
26
|
|
22
|
-
|
27
|
+
```
|
28
|
+
gem install datapackage
|
29
|
+
```
|
23
30
|
|
24
|
-
##
|
31
|
+
## Reading a Data Package
|
25
32
|
|
26
33
|
Require the gem, if you need to:
|
27
34
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
package = DataPackage::Package.new( "http://example.org/datasets/a" )
|
33
|
-
|
34
|
-
This assumes that `http://example.org/datasets/a/datapackage.json` exists, or specifically load a JSON file:
|
35
|
-
|
36
|
-
package = DataPackage::Package.new( "http://example.org/datasets/a/datapackage.json" )
|
37
|
-
|
38
|
-
Similarly you can load a package from a local JSON file, or specify a directory:
|
39
|
-
|
40
|
-
package = DataPackage::Package.new( "/my/data/package" )
|
41
|
-
package = DataPackage::Package.new( "/my/data/package/datapackage.json" )
|
42
|
-
|
43
|
-
There are a set of helper methods for accessing data from the package, e.g:
|
44
|
-
|
45
|
-
package = DataPackage::Package.new( "/my/data/package" )
|
46
|
-
package.name
|
47
|
-
package.title
|
48
|
-
package.licenses
|
49
|
-
package.resources
|
50
|
-
|
51
|
-
These currently just return the raw JSON structure, but this might change in future.
|
52
|
-
|
53
|
-
## Package Validation
|
54
|
-
|
55
|
-
The library supports validating packages. It can be used to validate both the metadata for the package (`datapackage.json`)
|
56
|
-
and the integrity of the package itself, e.g. whether the data files exist.
|
57
|
-
|
58
|
-
### Validating a Package
|
59
|
-
|
60
|
-
Quickly checking the validity of a package can be achieve as follows:
|
35
|
+
```ruby
|
36
|
+
require 'datapackage.rb'
|
37
|
+
```
|
61
38
|
|
62
|
-
|
63
|
-
|
64
|
-
To expose more detail on errors and warnings:
|
39
|
+
Parsing a Data Package from a remote location:
|
65
40
|
|
66
|
-
|
41
|
+
```ruby
|
42
|
+
package = DataPackage::Package.new( "http://example.org/datasets/a" )
|
43
|
+
```
|
67
44
|
|
68
|
-
This
|
69
|
-
Message objects are formatted as follows:
|
70
|
-
|
71
|
-
{
|
72
|
-
:type => :metadata|:integrity,
|
73
|
-
:message => "message for user",
|
74
|
-
:fragment => "/path/to/responsible/element"
|
75
|
-
}
|
76
|
-
|
77
|
-
It is possible to treat all warnings as errors by performing strict validation:
|
78
|
-
|
79
|
-
package.valid?(true)
|
80
|
-
|
81
|
-
Examples of warnings might include notes on missing metadata elements (e.g. package `licenses`) which are not required by the
|
82
|
-
DataPackage specification but which SHOULD be included.
|
83
|
-
|
84
|
-
Warnings are currently generated for:
|
85
|
-
|
86
|
-
* Missing `README.md` files from packages
|
87
|
-
* Missing `licenses` key from `datapackage.json`
|
88
|
-
* Missing `datapackage_version` key from `datapackage.json`
|
89
|
-
|
90
|
-
### Selecting a Validation Profile
|
91
|
-
|
92
|
-
The library contains two validation classes, one for the core Data Package specification and the other for the Simple Data Format
|
93
|
-
rules. By default the library uses the more liberal Data Package rules.
|
94
|
-
|
95
|
-
The required profile can be specified in one of two ways. Either as a parameter to the validation methods:
|
96
|
-
|
97
|
-
package.valid?(:datapackage)
|
98
|
-
package.valid?(:simpledataformat)
|
99
|
-
package.validate(:datapackage)
|
100
|
-
package.validate(:simpledataformat)
|
101
|
-
|
102
|
-
Or, by using a `DataPackage::Validation` class:
|
45
|
+
This assumes that `http://example.org/datasets/a/datapackage.json` exists, or specifically load a JSON file:
|
103
46
|
|
104
|
-
|
105
|
-
|
106
|
-
|
47
|
+
```ruby
|
48
|
+
package = DataPackage::Package.new( "http://example.org/datasets/a/datapackage.json" )
|
49
|
+
```
|
107
50
|
|
108
|
-
|
51
|
+
Similarly you can load a package from a local JSON file, or specify a directory:
|
109
52
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
SDF is essentially a profile of DataPackage which includes some additional restrictions on
|
115
|
-
how data should be published and described. For example all data is to be published as CSV files.
|
53
|
+
```ruby
|
54
|
+
package = DataPackage::Package.new( "/my/data/package" )
|
55
|
+
package = DataPackage::Package.new( "/my/data/package/datapackage.json" )
|
56
|
+
```
|
116
57
|
|
117
|
-
|
58
|
+
There are a set of helper methods for accessing data from the package, e.g:
|
118
59
|
|
119
|
-
|
120
|
-
|
60
|
+
```ruby
|
61
|
+
package = DataPackage::Package.new( "/my/data/package" )
|
62
|
+
package.name
|
63
|
+
package.title
|
64
|
+
package.description
|
65
|
+
package.homepage
|
66
|
+
package.license
|
67
|
+
```
|
121
68
|
|
122
|
-
|
69
|
+
## Creating a Data Package
|
123
70
|
|
124
|
-
|
125
|
-
|
71
|
+
```ruby
|
72
|
+
package = DataPackage::Package.new
|
126
73
|
|
127
|
-
|
128
|
-
|
74
|
+
package.name = 'my_sleep_duration'
|
75
|
+
package.resources = [
|
76
|
+
{'name': 'data'}
|
77
|
+
]
|
129
78
|
|
130
|
-
|
79
|
+
resource = package.resources[0]
|
80
|
+
resource.descriptor['data'] = [
|
81
|
+
7, 8, 5, 6, 9, 7, 8
|
82
|
+
]
|
131
83
|
|
132
|
-
|
133
|
-
|
84
|
+
File.open('datapackage.json', 'w') do |f|
|
85
|
+
f.write(package.to_json)
|
86
|
+
end
|
134
87
|
|
135
|
-
|
88
|
+
# {"name": "my_sleep_duration", "resources": [{"name": "data", "data": [7, 8, 5, 6, 9, 7, 8]}]}
|
89
|
+
```
|
136
90
|
|
137
|
-
|
138
|
-
* (`:simpledataformat`) All resources must be CSV files
|
139
|
-
* (`:simpledataformat`) All resources must have a valid JSON Table Schema
|
140
|
-
* (`:simpledataformat`) CSV `dialect` descriptions must be valid
|
141
|
-
* (`:simpledataformat`) All fields declared in the schema must be present in the CSV file
|
142
|
-
* (`:simpledataformat`) All fields present in the CSV file must be present in the schema
|
91
|
+
## Validating a Data Package
|
143
92
|
|
144
|
-
|
93
|
+
```ruby
|
94
|
+
package = DataPackage::Package.new('http://data.okfn.org/data/core/gdp/datapackage.json')
|
145
95
|
|
146
|
-
|
96
|
+
package.valid?
|
97
|
+
#=> true
|
98
|
+
package.errors
|
99
|
+
#=> [] # An array of errors
|
100
|
+
```
|
147
101
|
|
148
|
-
|
102
|
+
## Using a different schema
|
149
103
|
|
150
|
-
|
151
|
-
provided to the constructor of a `DataPackage::Validator` object, this can be used to map schema names to custom
|
152
|
-
schemas.
|
104
|
+
By default, the gem uses the standard [Data Package Schema](http://specs.frictionlessdata.io/data-packages/), but alternative schemas are available.
|
153
105
|
|
154
|
-
|
155
|
-
|
156
|
-
For example to create a new validation profile called `my-validation-rules` and then apply it:
|
106
|
+
### Schemas in the local cache
|
157
107
|
|
158
|
-
|
159
|
-
:schema => {
|
160
|
-
:my-validation-rules => "/path/to/json/schema.json"
|
161
|
-
}
|
162
|
-
}
|
163
|
-
package = DataPackage::Package.new( url )
|
164
|
-
package.valid?(:my-validation-rules)
|
108
|
+
The gem comes with schemas for the standard Data Package Schema, as well as the [Tabular Data Package Schema](http://specs.frictionlessdata.io/tabular-data-package/), and the [Fiscal Data Package Schema](http://fiscal.dataprotocols.org/spec/). These can be referred to via an identifier, expressed as a symbol.
|
165
109
|
|
166
|
-
|
167
|
-
|
110
|
+
```ruby
|
111
|
+
package = DataPackage::Package.new(nil, :tabular) # Or :fiscal
|
112
|
+
```
|
168
113
|
|
169
|
-
|
114
|
+
### Schemas from elsewhere
|
170
115
|
|
171
|
-
|
172
|
-
:schema => {
|
173
|
-
:my-validation-rules => "/path/to/json/schema.json"
|
174
|
-
}
|
175
|
-
}
|
176
|
-
validator = DataPackage::SimpleDataFormatValidator(:my-validation-rules, opts)
|
177
|
-
validator.valid?( package )
|
116
|
+
If you have a schema stored in an alternative registry, you can pass a `registry_url` option to the initializer.
|
178
117
|
|
179
|
-
|
180
|
-
|
118
|
+
```ruby
|
119
|
+
package = DataPackage::Package.new(nil, :identifier, {registry_url: 'http://example.org/my-registry.csv'} )
|
120
|
+
```
|
181
121
|
|
182
|
-
|
183
|
-
|
184
|
-
The built-in schema files can also be overridden in this way, e.g. by specifying an alternate location for the `:datapackage` schema.
|
122
|
+
## Developer notes
|
185
123
|
|
186
|
-
|
124
|
+
These notes are intended to help people that want to contribute to this package itself. If you just want to use it, you can safely ignore them.
|
187
125
|
|
188
|
-
|
126
|
+
### Updating the local schemas cache
|
189
127
|
|
190
|
-
|
128
|
+
We cache the schemas from https://github.com/dataprotocols/schemas using git-subtree. To update it, use:
|
191
129
|
|
192
|
-
|
193
|
-
|
130
|
+
```
|
131
|
+
git subtree pull --prefix datapackage/schemas https://github.com/dataprotocols/schemas.git master --squash
|
132
|
+
```
|
data/lib/datapackage/package.rb
CHANGED
@@ -1,160 +1,181 @@
|
|
1
1
|
require 'open-uri'
|
2
2
|
|
3
3
|
module DataPackage
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
4
|
+
class Package < Hash
|
5
|
+
attr_reader :opts, :errors
|
6
|
+
attr_writer :resources
|
7
|
+
|
8
|
+
# Parse or create a data package
|
9
|
+
#
|
10
|
+
# Supports reading data from JSON file, directory, and a URL
|
11
|
+
#
|
12
|
+
# package:: Hash or a String
|
13
|
+
# schema:: Hash, Symbol or String
|
14
|
+
# opts:: Options used to customize reading and parsing
|
15
|
+
def initialize(package = nil, schema = :base, opts = {})
|
16
|
+
@opts = opts
|
17
|
+
@schema = DataPackage::Schema.new(schema || :base)
|
18
|
+
@dead_resources = []
|
19
|
+
|
20
|
+
self.merge! parse_package(package)
|
21
|
+
define_properties!
|
22
|
+
load_resources!
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse_package(package)
|
26
|
+
# TODO: base directory/url
|
27
|
+
if package.nil?
|
28
|
+
{}
|
29
|
+
elsif package.class == Hash
|
30
|
+
package
|
31
|
+
else
|
32
|
+
read_package(package)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the directory for a local file package or base url for a remote
|
37
|
+
# Returns nil for an in-memory object (because it has no base as yet)
|
38
|
+
def base
|
39
|
+
# user can override base
|
40
|
+
return @opts[:base] if @opts[:base]
|
41
|
+
return '' unless @location
|
42
|
+
# work out base directory or uri
|
43
|
+
if local?
|
44
|
+
return File.dirname(@location)
|
45
|
+
else
|
46
|
+
return @location.split('/')[0..-2].join('/')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Is this a local package? Returns true if created from an in-memory object or a file/directory reference
|
51
|
+
def local?
|
52
|
+
return @local if @local
|
53
|
+
return !@location.start_with?('http') if @location
|
54
|
+
true
|
55
|
+
end
|
56
|
+
|
57
|
+
def resources
|
58
|
+
update_resources!
|
59
|
+
@resources
|
60
|
+
end
|
61
|
+
|
62
|
+
def property(property, default = nil)
|
63
|
+
self[property] || default
|
64
|
+
end
|
65
|
+
|
66
|
+
def valid?
|
67
|
+
validate
|
68
|
+
@valid
|
69
|
+
end
|
70
|
+
|
71
|
+
def validate
|
72
|
+
@errors = @schema.validation_errors(self)
|
73
|
+
@valid = @schema.valid?(self)
|
74
|
+
end
|
75
|
+
|
76
|
+
def resource_exists?(location)
|
77
|
+
@dead_resources.include?(location)
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_json
|
81
|
+
self.to_json
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def define_properties!
|
87
|
+
(@schema['properties'] || {}).each do |k, v|
|
88
|
+
next if k == 'resources'
|
89
|
+
define_singleton_method("#{k.to_sym}=", proc { |p| set_property(k, p) })
|
90
|
+
define_singleton_method(k.to_sym.to_s, proc { property k, default_value(v) })
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def load_resources!
|
95
|
+
@resources = (self['resources'] || [])
|
96
|
+
update_resources!
|
97
|
+
end
|
98
|
+
|
99
|
+
def update_resources!
|
100
|
+
@resources.map! do |resource|
|
101
|
+
begin
|
102
|
+
load_resource(resource)
|
103
|
+
rescue
|
104
|
+
@dead_resources << resource['path']
|
105
|
+
nil
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def load_resource(resource)
|
111
|
+
if resource.is_a?(Resource)
|
112
|
+
resource
|
113
|
+
else
|
114
|
+
Resource.load(resource, base)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def default_value(schema_data)
|
119
|
+
case schema_data['type']
|
120
|
+
when 'string'
|
121
|
+
nil
|
122
|
+
when 'array'
|
123
|
+
[]
|
124
|
+
when 'object'
|
125
|
+
{}
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def set_property(key, value)
|
130
|
+
self[key] = value
|
131
|
+
end
|
132
|
+
|
133
|
+
def read_package(package)
|
134
|
+
if is_directory?(package)
|
135
|
+
package = File.join(package, opts[:default_filename] || 'datapackage.json')
|
136
|
+
elsif is_containing_url?(package)
|
137
|
+
package = URI.join(package, 'datapackage.json')
|
138
|
+
end
|
139
|
+
|
140
|
+
@location = package.to_s
|
141
|
+
|
142
|
+
if File.extname(package.to_s) == '.zip'
|
143
|
+
unzip_package(package)
|
144
|
+
else
|
145
|
+
JSON.parse open(package).read
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def is_directory?(package)
|
150
|
+
!package.start_with?('http') && File.directory?(package)
|
151
|
+
end
|
152
|
+
|
153
|
+
def is_containing_url?(package)
|
154
|
+
package.start_with?('http') && !package.end_with?('datapackage.json', 'datapackage.zip')
|
155
|
+
end
|
156
|
+
|
157
|
+
def write_to_tempfile(url)
|
158
|
+
tempfile = Tempfile.new('datapackage')
|
159
|
+
tempfile.write(open(url).read)
|
160
|
+
tempfile.rewind
|
161
|
+
tempfile
|
162
|
+
end
|
163
|
+
|
164
|
+
def unzip_package(package)
|
165
|
+
package = write_to_tempfile(package) if package.start_with?('http')
|
166
|
+
dir = Dir.mktmpdir
|
167
|
+
Zip::File.open(package) do |zip_file|
|
168
|
+
# Extract all the files
|
169
|
+
zip_file.each { |entry| entry.extract("#{dir}/#{File.basename entry.name}") }
|
170
|
+
# Get and parse the datapackage metadata
|
171
|
+
entry = zip_file.glob("*/#{opts[:default_filename] || 'datapackage.json'}").first
|
172
|
+
package = JSON.parse(entry.get_input_stream.read)
|
173
|
+
end
|
174
|
+
# Set the base dir to the directory we unzipped to
|
175
|
+
@opts[:base] = dir
|
176
|
+
# This is now a local file, not a URL
|
177
|
+
@local = true
|
178
|
+
package
|
159
179
|
end
|
160
|
-
end
|
180
|
+
end
|
181
|
+
end
|