stac 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.
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module STAC
4
+ # Represents \STAC extent object, which describes the spatio-temporal extents of a Collection.
5
+ #
6
+ # Specification: https://github.com/radiantearth/stac-spec/blob/master/collection-spec/collection-spec.md#extent-object
7
+ class Extent
8
+ # Describes the spatial extents of a Collection
9
+ class Spatial
10
+ # Deserializes a Spatial from a Hash.
11
+ def self.from_hash(hash)
12
+ new(**hash.transform_keys(&:to_sym))
13
+ end
14
+
15
+ attr_accessor :bbox, :extra
16
+
17
+ def initialize(bbox:, **extra)
18
+ @bbox = bbox
19
+ @extra = extra.transform_keys(&:to_s)
20
+ end
21
+
22
+ # Serializes self to a Hash.
23
+ def to_h
24
+ {
25
+ 'bbox' => bbox,
26
+ }.merge(extra)
27
+ end
28
+ end
29
+
30
+ # Describes the temporal extents of a Collection.
31
+ class Temporal
32
+ # Deserializes a Temporal from a Hash.
33
+ def self.from_hash(hash)
34
+ new(**hash.transform_keys(&:to_sym))
35
+ end
36
+
37
+ attr_accessor :interval, :extra
38
+
39
+ def initialize(interval:, **extra)
40
+ @interval = interval
41
+ @extra = extra.transform_keys(&:to_s)
42
+ end
43
+
44
+ # Serializes self to a Hash.
45
+ def to_h
46
+ {
47
+ 'interval' => interval,
48
+ }.merge(extra)
49
+ end
50
+ end
51
+
52
+ class << self
53
+ # Deserializes an Extent from a Hash.
54
+ def from_hash(hash)
55
+ transformed = hash.transform_keys(&:to_sym)
56
+ transformed[:spatial] = Spatial.from_hash(transformed.fetch(:spatial))
57
+ transformed[:temporal] = Temporal.from_hash(transformed.fetch(:temporal))
58
+ new(**transformed)
59
+ end
60
+ end
61
+
62
+ attr_accessor :spatial, :temporal, :extra
63
+
64
+ def initialize(spatial:, temporal:, **extra)
65
+ @spatial = spatial
66
+ @temporal = temporal
67
+ @extra = extra.transform_keys(&:to_s)
68
+ end
69
+
70
+ # Serializes self to a Hash.
71
+ def to_h
72
+ {
73
+ 'spatial' => spatial.to_h,
74
+ 'temporal' => temporal.to_h,
75
+ }.merge(extra)
76
+ end
77
+ end
78
+ end
data/lib/stac/item.rb ADDED
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'asset'
4
+ require_relative 'errors'
5
+ require_relative 'properties'
6
+ require_relative 'stac_object'
7
+
8
+ module STAC
9
+ # Represents \STAC item.
10
+ #
11
+ # \STAC \Item Specification: https://github.com/radiantearth/stac-spec/tree/master/item-spec
12
+ class Item < STAC::STACObject
13
+ self.type = 'Feature'
14
+
15
+ class << self
16
+ def from_hash(hash)
17
+ h = hash.dup
18
+ h['properties'] = Properties.from_hash(h.fetch('properties'))
19
+ h['assets'] = h.fetch('assets').transform_values { |v| Asset.from_hash(v) }
20
+ super(h)
21
+ rescue KeyError => e
22
+ raise ArgumentError, "required field not found: #{e.key}"
23
+ end
24
+ end
25
+
26
+ attr_accessor :geometry, :bbox, :properties, :assets, :collection_id
27
+
28
+ def initialize(
29
+ id:, geometry:, properties:, links:, assets:, bbox: nil, collection: nil, stac_extensions: nil, **extra
30
+ )
31
+ super(id: id, links: links, stac_extensions: stac_extensions, **extra)
32
+ @geometry = geometry
33
+ @properties = properties
34
+ @assets = assets
35
+ @bbox = bbox
36
+ case collection
37
+ when Collection
38
+ self.collection = collection
39
+ else
40
+ @collection_id = collection
41
+ end
42
+ end
43
+
44
+ def to_h
45
+ super.merge(
46
+ {
47
+ 'geometry' => geometry, # required but nullable
48
+ },
49
+ ).merge(
50
+ {
51
+ 'bbox' => bbox,
52
+ 'properties' => properties.to_h,
53
+ 'assets' => assets.transform_values(&:to_h),
54
+ 'collection' => collection_id,
55
+ }.compact,
56
+ )
57
+ end
58
+
59
+ # Returns datetime from #properties.
60
+ def datetime
61
+ properties.datetime
62
+ end
63
+
64
+ # Returns a rel="collection" link as a collection object if it exists.
65
+ def collection
66
+ link = find_link(rel: 'collection')
67
+ link&.target
68
+ end
69
+
70
+ # Overwrites rel="collection" link and #collection_id attribute.
71
+ def collection=(collection)
72
+ raise ArgumentError, 'collection must have a rel="self" link' unless (collection_href = collection.self_href)
73
+
74
+ @collection_id = collection.id
75
+ collection_link = Link.new(
76
+ rel: 'collection',
77
+ href: collection_href,
78
+ type: 'application/json',
79
+ title: collection.title,
80
+ )
81
+ remove_link(rel: 'collection')
82
+ add_link(collection_link)
83
+ end
84
+ end
85
+ end
data/lib/stac/link.rb ADDED
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'uri'
5
+ require_relative 'errors'
6
+
7
+ module STAC
8
+ # Represents \STAC link object, which describes a relationship with another entity.
9
+ class Link
10
+ class << self
11
+ # Deserializes a Link from a Hash.
12
+ def from_hash(hash)
13
+ new(**hash.transform_keys(&:to_sym))
14
+ end
15
+ end
16
+
17
+ attr_accessor :rel, :href, :type, :title, :extra
18
+
19
+ # Owner object of this link.
20
+ attr_accessor :owner
21
+
22
+ attr_writer :resolver # :nodoc:
23
+
24
+ def initialize(rel:, href:, type: nil, title: nil, **extra)
25
+ @rel = rel
26
+ @href = href
27
+ @type = type
28
+ @title = title
29
+ @extra = extra.transform_keys(&:to_s)
30
+ end
31
+
32
+ # Serializes self to a Hash.
33
+ def to_h
34
+ {
35
+ 'rel' => rel,
36
+ 'href' => href,
37
+ 'type' => type,
38
+ 'title' => title,
39
+ }.merge(extra).compact
40
+ end
41
+
42
+ # Returns the absolute HREF for this link.
43
+ #
44
+ # When it could not assemble the absolute HREF, it returns nil.
45
+ def absolute_href
46
+ if URI(href).absolute?
47
+ href
48
+ elsif (base_href = owner&.self_href)
49
+ Pathname(base_href).dirname.join(href).to_s
50
+ end
51
+ end
52
+
53
+ # Returns a \STAC object resolved from HREF.
54
+ #
55
+ # When it could not assemble the absolute HREF, it returns nil.
56
+ def target
57
+ @target ||= if (url = absolute_href)
58
+ object = resolver.resolve(url)
59
+ object.self_href = url
60
+ object
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def resolver
67
+ @resolver ||= ObjectResolver.new
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative 'catalog'
5
+ require_relative 'collection'
6
+ require_relative 'default_http_client'
7
+ require_relative 'errors'
8
+ require_relative 'item'
9
+
10
+ module STAC
11
+ # Resolves a \STAC object from a URL.
12
+ class ObjectResolver
13
+ RESOLVABLES = [Catalog, Collection, Item].freeze # :nodoc:
14
+
15
+ class << self
16
+ # Sets the default HTTP client.
17
+ #
18
+ # HTTP client must implement method `get: (URI uri) -> String,` which fetches the URI resource through HTTP.
19
+ attr_writer :default_http_client
20
+
21
+ # Returns the default HTTP client.
22
+ def default_http_client
23
+ @default_http_client ||= DefaultHTTPClient.new
24
+ end
25
+ end
26
+
27
+ attr_reader :http_client
28
+
29
+ def initialize(http_client: self.class.default_http_client)
30
+ @http_client = http_client
31
+ end
32
+
33
+ # Reads a JSON from the given URL and returns a \STAC object resolved from it.
34
+ #
35
+ # Supports the following URL scheme:
36
+ # - http
37
+ # - https
38
+ # - file
39
+ #
40
+ # Raises:
41
+ # - STAC::UnknownURISchemeError when a URL with unsupported scheme was given
42
+ # - STAC::TypeError when it could not resolve any \STAC objects
43
+ def resolve(url)
44
+ str = read(url)
45
+ hash = JSON.parse(str)
46
+ klass = RESOLVABLES.find { |c| c.type == hash['type'] }
47
+ raise TypeError, "unknown STAC object type: #{hash['type']}" unless klass
48
+
49
+ klass.from_hash(hash)
50
+ end
51
+
52
+ private
53
+
54
+ def read(url)
55
+ uri = URI.parse(url)
56
+ case uri
57
+ when URI::HTTP
58
+ http_client.get(uri)
59
+ when URI::File
60
+ File.read(uri.path.to_s)
61
+ else
62
+ raise UnknownURISchemeError, "unknown URI scheme: #{url}"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module STAC
6
+ # Represents \STAC properties object, which is additional metadata for Item.
7
+ #
8
+ # Specification: https://github.com/radiantearth/stac-spec/blob/master/item-spec/item-spec.md#properties-object
9
+ class Properties
10
+ class << self
11
+ # Deserializes a Properties from a Hash.
12
+ def from_hash(hash)
13
+ h = hash.transform_keys(&:to_sym)
14
+ h[:datetime] = h[:datetime] ? Time.iso8601(h[:datetime]) : nil
15
+ new(**h)
16
+ end
17
+ end
18
+
19
+ attr_accessor :datetime, :extra
20
+
21
+ def initialize(datetime:, **extra)
22
+ @datetime = datetime
23
+ @extra = extra.transform_keys(&:to_s)
24
+ end
25
+
26
+ # Serializes self to a Hash.
27
+ def to_h
28
+ {
29
+ 'datetime' => datetime&.iso8601,
30
+ }.merge(extra)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module STAC
4
+ # Represents \STAC provider object, which provides information about a provider.
5
+ #
6
+ # Specicication: https://github.com/radiantearth/stac-spec/blob/master/collection-spec/collection-spec.md#provider-object
7
+ class Provider
8
+ class << self
9
+ # Deserializes a Provider from a Hash.
10
+ def from_hash(hash)
11
+ new(**hash.transform_keys(&:to_sym))
12
+ end
13
+ end
14
+
15
+ attr_accessor :name, :description, :roles, :url, :extra
16
+
17
+ def initialize(name:, description: nil, roles: nil, url: nil, **extra)
18
+ @name = name
19
+ @description = description
20
+ @roles = roles
21
+ @url = url
22
+ @extra = extra.transform_keys(&:to_s)
23
+ end
24
+
25
+ # Serializes self to a Hash.
26
+ def to_h
27
+ {
28
+ 'name' => name,
29
+ 'description' => description,
30
+ 'roles' => roles,
31
+ 'url' => url,
32
+ }.merge(extra).compact
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module STAC
4
+ SPEC_VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative 'errors'
5
+ require_relative 'link'
6
+ require_relative 'spec_version'
7
+
8
+ module STAC
9
+ # Base class for \STAC objects (i.e. Catalog, Collection and Item).
10
+ class STACObject
11
+ class << self
12
+ attr_accessor :type # :nodoc:
13
+
14
+ # Base method to deserialize shared fields from a Hash.
15
+ #
16
+ # Raises ArgumentError when any required fields are missing.
17
+ def from_hash(hash)
18
+ raise TypeError, "type field is not 'Catalog': #{hash['type']}" if hash.fetch('type') != type
19
+
20
+ transformed = hash.transform_keys(&:to_sym).except(:type, :stac_version)
21
+ transformed[:links] = transformed.fetch(:links).map { |link| Link.from_hash(link) }
22
+ new(**transformed)
23
+ rescue KeyError => e
24
+ raise ArgumentError, "required field not found: #{e.key}"
25
+ end
26
+ end
27
+
28
+ attr_accessor :id, :stac_extensions, :extra
29
+
30
+ attr_reader :links
31
+
32
+ def initialize(id:, links:, stac_extensions: nil, **extra)
33
+ @id = id
34
+ @links = []
35
+ links.each do |link|
36
+ add_link(link) # to set `owner`
37
+ end
38
+ @stac_extensions = stac_extensions
39
+ @extra = extra.transform_keys(&:to_s)
40
+ end
41
+
42
+ def type
43
+ self.class.type
44
+ end
45
+
46
+ # Serializes self to a Hash.
47
+ def to_h
48
+ {
49
+ 'type' => type,
50
+ 'stac_version' => SPEC_VERSION,
51
+ 'stac_extensions' => stac_extensions,
52
+ 'id' => id,
53
+ 'links' => links.map(&:to_h),
54
+ }.merge(extra).compact
55
+ end
56
+
57
+ # Serializes self to a JSON string.
58
+ def to_json(...)
59
+ to_h.to_json(...)
60
+ end
61
+
62
+ # Adds a link with setting Link#owner as self.
63
+ def add_link(link)
64
+ link.owner = self
65
+ links << link
66
+ end
67
+
68
+ # Reterns HREF of the rel="self" link.
69
+ def self_href
70
+ find_link(rel: 'self')&.href
71
+ end
72
+
73
+ # Adds a link with the give HREF as rel="self".
74
+ #
75
+ # When any ref="self" links already exist, removes them.
76
+ def self_href=(absolute_href)
77
+ self_link = Link.new(rel: 'self', href: absolute_href, type: 'application/json')
78
+ remove_link(rel: 'self')
79
+ add_link(self_link)
80
+ end
81
+
82
+ private
83
+
84
+ def find_link(rel:)
85
+ links.find { |link| link.rel == rel }
86
+ end
87
+
88
+ def remove_link(rel:)
89
+ links.reject! { |link| link.rel == rel }
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module STAC
4
+ VERSION = '0.1.0'
5
+ end
data/lib/stac.rb ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'stac/object_resolver'
4
+ require_relative 'stac/version'
5
+
6
+ # Gem namespace.
7
+ #
8
+ # Provides some utility methods.
9
+ module STAC
10
+ class << self
11
+ # Returns a \STAC object resolved from the given file path.
12
+ def from_file(path)
13
+ from_url("file://#{File.expand_path(path)}")
14
+ end
15
+
16
+ # Returns a \STAC object resolved from the given URL.
17
+ #
18
+ # When the resolved object does not have rel="self" link, adds a rel="self" link with the give url.
19
+ def from_url(url, resolver: ObjectResolver.new)
20
+ object = resolver.resolve(url)
21
+ object.self_href = url unless object.self_href
22
+ object
23
+ end
24
+ end
25
+ end
data/sig/open-uri.rbs ADDED
@@ -0,0 +1,8 @@
1
+ module OpenURI
2
+ class HTTPError < StandardError
3
+ end
4
+ end
5
+
6
+ module URI
7
+ def read: (?Hash[String, String] options) -> String
8
+ end
@@ -0,0 +1,18 @@
1
+ module STAC
2
+ class Asset
3
+ def self.from_hash: (Hash[String, untyped] hash) -> Asset
4
+
5
+ attr_accessor href: String
6
+ attr_accessor title: String?
7
+ attr_accessor description: String?
8
+ attr_accessor type: String?
9
+ attr_accessor roles: Array[String]?
10
+ attr_accessor extra: Hash[String, untyped]
11
+
12
+ def initialize: (
13
+ href: String, ?title: String?, ?description: String?, ?type: String?, ?roles: Array[String]?, **untyped
14
+ ) -> void
15
+
16
+ def to_h: -> Hash[String, untyped]
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ module STAC
2
+ class Catalog < STACObject
3
+ def self.from_hash: (Hash[String, untyped] hash) -> Catalog
4
+
5
+ attr_accessor description: String
6
+ attr_accessor title: String?
7
+
8
+ def initialize: (
9
+ id: String, description: String, links: Array[Link], ?title: String?, ?stac_extensions: Array[String]?, **untyped
10
+ ) -> void
11
+
12
+ def to_h: -> Hash[String, untyped]
13
+ def children: -> Enumerator::Lazy[Catalog | Collection, void]
14
+ def collections: -> Enumerator::Lazy[Collection, void]
15
+ def find_child: (String id, ?recursive: bool) -> (Catalog | Collection | nil)
16
+ def items: -> Enumerator::Lazy[Item, void]
17
+ def all_items: -> Enumerator::Lazy[Item, void]
18
+ def find_item: (String id, ?recursive: bool) -> Item?
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ module STAC
2
+ class Collection < Catalog
3
+ def self.from_hash: (Hash[String, untyped] hash) -> Collection
4
+
5
+ attr_accessor license: String
6
+ attr_accessor extent: Extent
7
+ attr_accessor keywords: Array[String]?
8
+ attr_accessor providers: Array[Provider]?
9
+ attr_accessor summaries: Hash[String, untyped]?
10
+ attr_accessor assets: Hash[String, Asset]?
11
+
12
+ def initialize: (
13
+ id: String,
14
+ description: String,
15
+ links: Array[Link],
16
+ license: String,
17
+ extent: Extent,
18
+ ?title: String?,
19
+ ?keywords: Array[String]?,
20
+ ?providers: Array[Provider]?,
21
+ ?summaries: Hash[String, untyped]?,
22
+ ?assets: Hash[String, Asset]?,
23
+ ?stac_extensions: Array[String]?,
24
+ **untyped
25
+ ) -> void
26
+
27
+ def to_h: -> Hash[String, untyped]
28
+ end
29
+ end
@@ -0,0 +1,9 @@
1
+ module STAC
2
+ class DefaultHTTPClient
3
+ attr_reader options: Hash[String, String]
4
+
5
+ def initialize: (?Hash[String, String] options) -> void
6
+
7
+ def get: (URI uri) -> String
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ module STAC
2
+ class Error < StandardError
3
+ end
4
+
5
+ class TypeError < Error
6
+ end
7
+
8
+ class UnknownURISchemeError < Error
9
+ end
10
+
11
+ class HTTPError < Error
12
+ end
13
+ end
@@ -0,0 +1,35 @@
1
+ module STAC
2
+ class Extent
3
+ class Spatial
4
+ def self.from_hash: (Hash[String, untyped] hash) -> Spatial
5
+
6
+ attr_accessor bbox: Array[Array[Numeric]]
7
+ attr_accessor extra: Hash[String, untyped]
8
+
9
+ def initialize: (bbox: Array[Array[Numeric]], **untyped) -> void
10
+
11
+ def to_h: -> Hash[String, untyped]
12
+ end
13
+
14
+ class Temporal
15
+ def self.from_hash: (Hash[String, untyped] hash) -> Temporal
16
+
17
+ attr_accessor interval: Array[Array[String?]]
18
+ attr_accessor extra: Hash[String, untyped]
19
+
20
+ def initialize: (interval: Array[Array[String?]], **untyped) -> void
21
+
22
+ def to_h: -> Hash[String, untyped]
23
+ end
24
+
25
+ def self.from_hash: (Hash[String, untyped] hash) -> Extent
26
+
27
+ attr_accessor spatial: Spatial
28
+ attr_accessor temporal: Temporal
29
+ attr_accessor extra: Hash[String, untyped]
30
+
31
+ def initialize: (spatial: Spatial, temporal: Temporal, **untyped) -> void
32
+
33
+ def to_h: -> Hash[String, untyped]
34
+ end
35
+ end
data/sig/stac/item.rbs ADDED
@@ -0,0 +1,27 @@
1
+ module STAC
2
+ class Item < STACObject
3
+ def self.from_hash: (Hash[String, untyped] hash) -> Item
4
+
5
+ attr_accessor geometry: Hash[String, untyped]?
6
+ attr_accessor properties: Properties
7
+ attr_accessor assets: Hash[String, Asset]
8
+ attr_accessor bbox: Array[Numeric]?
9
+ attr_accessor collection_id: String?
10
+
11
+ def initialize: (
12
+ id: String,
13
+ geometry: Hash[String, untyped]?,
14
+ properties: Properties,
15
+ links: Array[Link],
16
+ assets: Hash[String, Asset],
17
+ ?bbox: Array[Numeric]?,
18
+ ?collection: String | Collection | nil,
19
+ ?stac_extensions: Array[String]?,
20
+ **untyped
21
+ ) -> void
22
+
23
+ def datetime: -> Time?
24
+ def collection: -> Collection?
25
+ def collection=: (Collection collection) -> void
26
+ end
27
+ end
data/sig/stac/link.rbs ADDED
@@ -0,0 +1,24 @@
1
+ module STAC
2
+ class Link
3
+ def self.from_hash: (Hash[String, untyped]) -> Link
4
+
5
+ attr_accessor rel: String
6
+ attr_accessor href: String
7
+ attr_accessor type: String?
8
+ attr_accessor title: String?
9
+ attr_accessor extra: Hash[String, untyped]
10
+ attr_accessor owner: STACObject | nil
11
+ @target: Catalog | Collection | Item | nil
12
+ @resolver: ObjectResolver
13
+
14
+ def initialize: (rel: String, href: String, ?type: String?, ?title: String?, **untyped) -> void
15
+
16
+ def to_h: -> Hash[String, untyped]
17
+ def absolute_href: -> String?
18
+ def target: -> (Catalog | Collection | Item | nil)
19
+
20
+ private
21
+
22
+ def resolver: -> ObjectResolver
23
+ end
24
+ end