reality 0.0.2 → 0.0.3

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.dokaz +1 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +538 -66
  5. data/bin/reality +9 -0
  6. data/config/demo.yml +3 -0
  7. data/data/wikidata-predicates.json +1 -0
  8. data/data/wikidata-predicates.yaml +2089 -0
  9. data/lib/reality.rb +26 -7
  10. data/lib/reality/config.rb +46 -0
  11. data/lib/reality/definitions/dictionaries.rb +67 -0
  12. data/lib/reality/definitions/helpers.rb +34 -0
  13. data/lib/reality/definitions/wikidata.rb +105 -0
  14. data/lib/reality/definitions/wikipedia_character.rb +17 -0
  15. data/lib/reality/definitions/wikipedia_city.rb +19 -0
  16. data/lib/reality/definitions/wikipedia_continent.rb +21 -0
  17. data/lib/reality/definitions/wikipedia_country.rb +23 -0
  18. data/lib/reality/definitions/wikipedia_musical_artist.rb +15 -0
  19. data/lib/reality/definitions/wikipedia_person.rb +17 -0
  20. data/lib/reality/entity.rb +152 -0
  21. data/lib/reality/entity/coercion.rb +76 -0
  22. data/lib/reality/entity/wikidata_predicates.rb +31 -0
  23. data/lib/reality/entity/wikipedia_type.rb +73 -0
  24. data/lib/reality/extras/geonames.rb +29 -0
  25. data/lib/reality/extras/open_weather_map.rb +63 -0
  26. data/lib/reality/geo.rb +122 -0
  27. data/lib/reality/infoboxer_templates.rb +8 -0
  28. data/lib/reality/list.rb +95 -0
  29. data/lib/reality/measure.rb +18 -12
  30. data/lib/reality/measure/unit.rb +5 -1
  31. data/lib/reality/methods.rb +16 -0
  32. data/lib/reality/pretty_inspect.rb +11 -0
  33. data/lib/reality/refinements.rb +26 -0
  34. data/lib/reality/shortcuts.rb +11 -0
  35. data/lib/reality/tz_offset.rb +64 -0
  36. data/lib/reality/util/formatters.rb +35 -0
  37. data/lib/reality/util/parsers.rb +53 -0
  38. data/lib/reality/version.rb +6 -0
  39. data/lib/reality/wikidata.rb +310 -0
  40. data/reality.gemspec +12 -3
  41. data/script/extract_wikidata_properties.rb +23 -0
  42. data/script/lib/nokogiri_more.rb +175 -0
  43. metadata +137 -7
  44. data/examples/all_countries.rb +0 -16
  45. data/lib/reality/country.rb +0 -283
@@ -1,15 +1,34 @@
1
1
  require 'infoboxer'
2
+ require 'yaml'
2
3
 
3
4
  module Reality
4
- require_relative 'reality/infoboxer_templates'
5
-
6
- # basic functionality
7
- %w[measure].each do |mod|
8
- require_relative "reality/#{mod}"
5
+ def self.require_(*modules)
6
+ modules.flatten.flat_map{|pattern|
7
+ Dir[File.expand_path("../reality/#{pattern}.rb", __FILE__)]
8
+ }.each(&Kernel.method(:require))
9
9
  end
10
10
 
11
+ # basic functionality
12
+ require_ %w[version refinements config measure geo tz_offset]
13
+ require_ %w[util/parsers util/formatters]
14
+
15
+ # engines
16
+ require_ %w[infoboxer_templates wikidata]
17
+ Infoboxer.user_agent = "Reality/#{VERSION} (https://github.com/molybdenum-99/reality; zverok.offline@gmail.com)"
18
+
11
19
  # entities
12
- %w[country].each do |mod|
13
- require_relative "reality/#{mod}"
20
+ require_ %w[entity list]
21
+ require_ %w[definitions/*]
22
+ require_ %w[methods]
23
+
24
+ extend Methods
25
+
26
+ def self.reload!
27
+ require_ %w[definitions/*]
14
28
  end
29
+
30
+ # extras
31
+ require_ %w[extras/open_weather_map extras/geonames]
32
+ include Extras::OpenWeatherMap
33
+ include Extras::Geonames
15
34
  end
@@ -0,0 +1,46 @@
1
+ require 'hashie'
2
+
3
+ module Reality
4
+ class Config
5
+ attr_reader :keys, :data
6
+
7
+ def initialize
8
+ @keys = {}
9
+ @data = {}.extend Hashie::Extensions::DeepFetch
10
+ end
11
+
12
+ def load(str)
13
+ if File.exists?(str)
14
+ str = File.read(str)
15
+ end
16
+
17
+ @data.update(YAML.load(str))
18
+ end
19
+
20
+ def fetch(*path)
21
+ data.deep_fetch(*path){
22
+ if (known = @keys[path])
23
+ fail KeyError, "Expected #{path.join('.')} to exist in config. It is #{known[:desc]}"
24
+ else
25
+ fail KeyError, "Expected #{path.join('.')} to exist in config."
26
+ end
27
+ }
28
+ end
29
+
30
+ def register(*path, **opts)
31
+ @keys[path] = opts
32
+ end
33
+ end
34
+
35
+ def Reality.config
36
+ @config ||= Config.new
37
+ end
38
+
39
+ def Reality.configure(cfg)
40
+ if cfg == :demo
41
+ config.load(File.expand_path('../../../config/demo.yml', __FILE__))
42
+ else
43
+ config.load(cfg)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,67 @@
1
+ module Reality
2
+ module Dictionaries
3
+ module_function
4
+
5
+ using Reality::Refinements
6
+
7
+ def countries
8
+ List.new(*countries_by_continents_cache.keys)
9
+ end
10
+
11
+ def continents
12
+ @continents ||=
13
+ Infoboxer.wp.get('Continent').
14
+ sections('Area and population').tables.first.
15
+ lookup(:TableHeading, index: 0).lookup(:Wikilink).
16
+ derp{|links| Entity::Coercion.coerce(links, [:entity])}
17
+ end
18
+
19
+ CITY_SYNONYMS = [
20
+ 'City',
21
+ 'Municipality',
22
+ 'Commune', # France
23
+ 'GCCSA/SUA' # Australia
24
+ ]
25
+
26
+ CITIES_PAGE_BY_COUNTRY =
27
+ Hash.new{|_, name| 'List of cities in %s' % name}.
28
+ merge(
29
+ 'United States' => 'List of United States cities by population',
30
+ 'Australia' => 'List of cities in Australia by population'
31
+ )
32
+
33
+ def cities_by_country(name)
34
+ page = Infoboxer.wp.get(CITIES_PAGE_BY_COUNTRY[name]) or
35
+ return Entity::List.new()
36
+
37
+ page.tables.map{|t|
38
+ t.heading_row or next []
39
+
40
+ idx = t.heading_row.children.
41
+ index{|c| c.text =~ /\bname\b/i || CITY_SYNONYMS.any?{|s| c.text.strip.start_with?(s)}} or next []
42
+
43
+ t.lookup(:TableCell, index: idx).lookup(:Wikilink)
44
+ }.flatten.
45
+ derp{|links| Entity::Coercion.coerce(links, [:entity])}
46
+ end
47
+
48
+ def countries_by_continent(name)
49
+ countries_by_continents_cache.select{|k, v| v == name}.map(&:first).
50
+ derp{|names| List.new(*names)}
51
+ end
52
+
53
+ def countries_by_continents_cache
54
+ @by_continents ||= Infoboxer.wp.
55
+ get('List of countries by continent').
56
+ sections.first.
57
+ sections.map{|s|
58
+ continent = s.heading.text_
59
+ s.tables.first.
60
+ lookup(:Wikilink, :bold?).map(&:link).
61
+ map{|country| [country, continent]}
62
+ }.flatten(1).
63
+ reject{|country, continent| country == 'Holy See'}. # it has [Vatican City]/[Holy See]
64
+ to_h
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,34 @@
1
+ require 'time_boots'
2
+
3
+ module Reality
4
+ # Just assorted "cool things", included into all entities
5
+ # Subject to change/refactor
6
+ module Helpers
7
+ def alive?
8
+ !birthday.nil? && date_of_death.nil?
9
+ end
10
+
11
+ def dead?
12
+ !date_of_death.nil?
13
+ end
14
+
15
+ def age_at(tm)
16
+ # TimeBoots fail with Time vs Date :(
17
+ #birthday && TimeBoots.year.measure(birthday, tm)
18
+
19
+ from = birthday || created_at || published_at
20
+
21
+ if from.month < tm.month || from.month == tm.month && from.day <= tm.day
22
+ tm.year - from.year
23
+ else
24
+ tm.year - from.year - 1
25
+ end
26
+ end
27
+
28
+ def age
29
+ age_at(Date.today)
30
+ end
31
+ end
32
+
33
+ Reality::Entity.include Helpers
34
+ end
@@ -0,0 +1,105 @@
1
+ module Reality
2
+ Entity::WikidataPredicates.define do
3
+ # Generic relations ------------------------------------------------
4
+ predicate 'P361', :part_of, [:entity]
5
+ predicate 'P527', :parts, [:entity] # aliases: :members
6
+
7
+ predicate 'P155', :follows, :entity
8
+ predicate 'P156', :precedes, :entity
9
+ predicate 'P571', :created_at, :date # TODO: aliases: :founded_at, :incepted_at
10
+
11
+ predicate 'P740', :location, :entity
12
+
13
+ predicate 'P737', :influenced_by, [:entity]
14
+
15
+ # Object features --------------------------------------------------
16
+ predicate 'P2048', :height, :measure, unit: 'm'
17
+
18
+ # Geography --------------------------------------------------------
19
+ predicate 'P625', :coord, :coord
20
+
21
+ predicate 'P30' , :continent, :entity
22
+ predicate 'P17' , :country, :entity
23
+ predicate 'P610', :highest_point, :entity
24
+
25
+ predicate 'P36' , :capital, :entity
26
+ predicate 'P150', :adm_divisions, [:entity]
27
+
28
+ predicate 'P47' , :neighbours, [:entity]
29
+
30
+ predicate 'P2046', :area, :measure, unit: 'km²'
31
+ predicate 'P2044', :elevation, :measure, unit: 'm'
32
+
33
+ # Economy and socilogy ---------------------------------------------
34
+ predicate 'P38', :currency, :entity
35
+ predicate 'P463', :organizations, [:entity]
36
+ predicate 'P2131', :gdp_nominal, :measure, unit: '$'
37
+ predicate 'P1082',:population, :measure, unit: 'person'
38
+
39
+ predicate 'P35', :head_of_state, :entity
40
+
41
+ # References -------------------------------------------------------
42
+ predicate 'P297', :iso2_code, :string
43
+ predicate 'P298', :iso3_code, :string
44
+ predicate 'P78', :tld, :string
45
+ predicate 'P474', :calling_code, :string
46
+ predicate 'P421', :tz_offset, :tz_offset
47
+
48
+ # People -----------------------------------------------------------
49
+ # personal
50
+ predicate 'P19', :birth_place, :entity
51
+ predicate 'P569', :birthday, :date
52
+ predicate 'P570', :date_of_death, :date
53
+ predicate 'P20', :place_of_death, :entity
54
+ predicate 'P21', :sex, :string
55
+ predicate 'P735', :given_name, :string
56
+
57
+ # family
58
+ predicate 'P26', :spouse, :entity
59
+ predicate 'P40', :children, [:entity]
60
+ predicate 'P22', :father, :entity
61
+
62
+ # social
63
+ predicate 'P551', :residence, :entity
64
+ predicate 'P27', :citizenship, :entity
65
+ predicate 'P39', :position, :string
66
+ predicate 'P106', :occupations, [:string]
67
+
68
+ # General creative works & workers ---------------------------------
69
+ predicate 'P577', :published_at, :date
70
+ predicate 'P136', :genres, [:string]
71
+ predicate 'P166', :awards, [:entity]
72
+ predicate 'P1411', :nominations, [:entity]
73
+ predicate 'P921', :work_subjects, [:string]
74
+ predicate 'P364', :original_languages, [:string]
75
+
76
+ # Music album ------------------------------------------------------
77
+ predicate 'P658', :tracks, [:string]
78
+ predicate 'P175', :performer, :entity
79
+ #predicate 'P175', :performers, [:entity] - TODO
80
+
81
+ # Companies --------------------------------------------------------
82
+ predicate 'P112', :founders, [:entity]
83
+ predicate 'P127', :owners, [:entity]
84
+ predicate 'P169', :ceo, :entity
85
+ predicate 'P1128', :employees_count, :measure, unit: 'person'
86
+ predicate 'P452', :industry, :string
87
+
88
+ # Software ---------------------------------------------------------
89
+ predicate 'P178', :developers, [:entity]
90
+ predicate 'P275', :licenses, [:string]
91
+ predicate 'P348', :version, :string
92
+
93
+ # Movies -----------------------------------------------------------
94
+ predicate 'P57', :directors, [:entity]
95
+ predicate 'P162', :producers, [:entity]
96
+ predicate 'P161', :actors, [:entity]
97
+
98
+ # Wehicles ---------------------------------------------------------
99
+ predicate 'P1029', :crew_members, [:entity]
100
+
101
+ # Fictional entities -----------------------------------------------
102
+ predicate 'P1080', :fictional_universe, :string
103
+ predicate 'P1441', :present_in_works, [:entity]
104
+ end
105
+ end
@@ -0,0 +1,17 @@
1
+ module Reality
2
+ module Character
3
+ extend Entity::WikipediaType
4
+
5
+ infobox_name 'Infobox character'
6
+
7
+ # FIXME: Both should be auto-parsed
8
+ infobox 'species', :species, :entity,
9
+ parse: ->(var){var.lookup(:Wikilink).first}
10
+
11
+ infobox 'affiliation', :affiliations, [:entity],
12
+ parse: ->(var){var.lookup(:Wikilink)}
13
+
14
+ infobox 'portrayer', :portrayer, :entity,
15
+ parse: ->(var){var.lookup(:Wikilink).first}
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ module Reality
2
+ module City
3
+ extend Entity::WikipediaType
4
+
5
+ infobox_name 'Infobox settlement',
6
+ 'Infobox city Japan',
7
+ 'Infobox Russian city', 'Infobox Russian town',
8
+ 'Infobox Russian inhabited locality',
9
+ 'Infobox Russian federal subject'
10
+
11
+ infobox 'name', :long_name, :string
12
+
13
+ infobox 'area_km2', :area, :measure, unit: 'km²'
14
+ infobox 'area_total_km2', :area, :measure, unit: 'km²'
15
+
16
+ infobox 'population_total', :population, :measure, unit: 'person'
17
+ infobox 'population_metro', :population_metro, :measure, unit: 'person'
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ module Reality
2
+ module Continent
3
+ extend Entity::WikipediaType
4
+
5
+ infobox_name 'Infobox continent', 'Infobox Continent' # FIXME: Infoboxer is a bit dumb about this
6
+
7
+ infobox 'area', :area, :measure, unit: 'km²',
8
+ parse: ->(var){
9
+ str = var.children.
10
+ templates(name: /^Convert$/i). # Infoboxer is dumb here, tooo :(
11
+ fetch('1').text
12
+
13
+ str = var.children.text if str.empty?
14
+ Util::Parse.scaled_number(str)
15
+ }
16
+
17
+ def countries
18
+ @countries ||= Dictionaries.countries_by_continent(name)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ module Reality
2
+ module Country
3
+ extend Entity::WikipediaType
4
+
5
+ infobox_name 'Infobox country'
6
+
7
+ infobox 'conventional_long_name', :long_name, :string
8
+ infobox 'area_km2', :area, :measure, unit: 'km²'
9
+
10
+ infobox 'GDP_PPP', :gdp_ppp, :measure, unit: '$',
11
+ parse: ->(var){
12
+ str = var.text.strip.sub(/^((Int|US)?\$|USD)/, '')
13
+ Util::Parse.scaled_number(str)
14
+ }
15
+
16
+ infobox 'population_census', :population, :measure, unit: 'person'
17
+ infobox 'population_estimate', :population, :measure, unit: 'person'
18
+
19
+ def cities
20
+ @cities ||= Dictionaries.cities_by_country(name)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ module Reality
2
+ module MusicalArtist
3
+ extend Entity::WikipediaType
4
+
5
+ infobox_name 'Infobox musical artist'
6
+
7
+ parse :albums, [:entity] do |page|
8
+ if (sec = page.sections('Discography').first)
9
+ sec.lookup(:UnorderedList).first.lookup(:Wikilink, :italic?)
10
+ else
11
+ nil
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module Reality
2
+ module Person
3
+ extend Entity::WikipediaType
4
+
5
+ infobox_name 'Infobox person'
6
+
7
+ # Singular artists, like Bjork or David Bowie
8
+ # TODO: should not repeat code from musical artist, but include it from module?
9
+ parse :albums, [:entity] do |page|
10
+ if (sec = page.sections('Discography').first)
11
+ sec.lookup(:UnorderedList).first.lookup(:Wikilink, :italic?)
12
+ else
13
+ nil
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,152 @@
1
+ module Reality
2
+ require_ %w[entity/coercion entity/wikidata_predicates entity/wikipedia_type]
3
+
4
+ class Entity
5
+ using Refinements
6
+
7
+ attr_reader :wikipage, :wikidata, :wikidata_id
8
+ attr_reader :values, :wikipedia_type
9
+
10
+ def initialize(name, wikipage: nil, wikidata: nil, wikidata_id: nil, load: false)
11
+ @name = name
12
+ @wikipage, @wikidata, @wikidata_id = wikipage, wikidata, wikidata_id
13
+ @values = {}
14
+
15
+ load! if load
16
+ after_load if @wikipage
17
+ end
18
+
19
+ def name
20
+ @wikipage ? @wikipage.title : @name
21
+ end
22
+
23
+ def inspect
24
+ if @wikipedia_type && @wikipedia_type.symbol
25
+ "#<#{self.class}#{loaded? ? '' : '?'}(#{name}):#{@wikipedia_type.symbol}>"
26
+ else
27
+ "#<#{self.class}#{loaded? ? '' : '?'}(#{name})>"
28
+ end
29
+ end
30
+
31
+ def _describe
32
+ load! unless loaded?
33
+ Util::Format.describe(inspect, values.map{|k,v| [k, v.inspect]})
34
+ end
35
+
36
+ def describe
37
+ puts _describe
38
+ nil
39
+ end
40
+
41
+ def to_s
42
+ name
43
+ end
44
+
45
+ def to_s?
46
+ # FIXME: fuuuuuuuu
47
+ "#{name.include?(',') ? '"' + name + '"' : name}#{loaded? ? '' : '?'}"
48
+ end
49
+
50
+ def load!
51
+ if @wikidata_id
52
+ @wikidata = Wikidata::Entity.fetch_by_id(@wikidata_id)
53
+ if @wikidata && @wikidata.en_wikipage
54
+ @wikipage = Infoboxer.wikipedia.get(@wikidata.en_wikipage)
55
+ end
56
+ else
57
+ @wikipage = Infoboxer.wikipedia.get(name)
58
+ if @wikipage
59
+ @wikidata = Wikidata::Entity.fetch(@wikipage.title).first
60
+ end
61
+ end
62
+ after_load
63
+ self
64
+ end
65
+
66
+ def setup!(wikipage: nil, wikidata: nil)
67
+ @wikipage, @wikidata = wikipage, wikidata
68
+ after_load if @wikipage
69
+ end
70
+
71
+ def loaded?
72
+ !!@wikipage
73
+ end
74
+
75
+ # Don't try to convert me!
76
+ UNSUPPORTED_METHODS = [:to_hash, :to_ary, :to_a, :to_str, :to_int]
77
+
78
+ def method_missing(sym, *arg, **opts, &block)
79
+ if arg.empty? && opts.empty? && !block && sym !~ /[=?!]/ &&
80
+ !UNSUPPORTED_METHODS.include?(sym)
81
+
82
+ load! unless loaded?
83
+
84
+ # now some new method COULD emerge while loading
85
+ if methods.include?(sym)
86
+ send(sym)
87
+ else
88
+ values[sym]
89
+ end
90
+ else
91
+ super
92
+ end
93
+ end
94
+
95
+ def respond_to?(sym)
96
+ sym !~ /[=?!]/ && !UNSUPPORTED_METHODS.include?(sym) || super
97
+ end
98
+
99
+ class << self
100
+ def load(name, type = nil)
101
+ Entity.new(name, load: true).tap{|entity|
102
+ return nil if entity.instance_variable_get('@wikipage').nil?
103
+ return nil if type && entity.wikipedia_type != type
104
+ }
105
+ end
106
+ end
107
+
108
+ #def to_h
109
+ #if respond_to?(:properties)
110
+ #properties.map{|sym|
111
+ #[sym, to_simple_type(self.send(sym))]
112
+ #}.to_h
113
+ #else
114
+ #{}
115
+ #end
116
+ #end
117
+
118
+ protected
119
+
120
+ def after_load
121
+ if @wikipage && !@wikipedia_type
122
+ if (@wikipedia_type = WikipediaType.for(self))
123
+ extend(@wikipedia_type)
124
+ end
125
+ end
126
+ if @wikidata
127
+ @values.update(WikidataPredicates.parse(@wikidata))
128
+ end
129
+ (@values.keys - methods).each do |sym|
130
+ define_singleton_method(sym){@values[sym]}
131
+ end
132
+ end
133
+
134
+ def to_simple_type(val)
135
+ case val
136
+ when nil, Numeric, String, Symbol
137
+ val
138
+ when Array
139
+ val.map{|v| to_simple_type(v)}
140
+ when Hash
141
+ val.map{|k, v| [to_simple_type(k), to_simple_type(v)]}.to_h
142
+ when Entity
143
+ val.to_s
144
+ when Reality::Measure
145
+ val.amount.to_i
146
+ else
147
+ fail ArgumentError, "Non-coercible value #{val.class}"
148
+ end
149
+ end
150
+
151
+ end
152
+ end