fias 0.0.1 → 0.0.2

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.
data/README.md CHANGED
@@ -16,33 +16,66 @@ gem 'fias', git: 'https://github.com/evilmartians/kladr.git'
16
16
 
17
17
  2. Скачать и распаковать базу ФИАС в tmp/fias (по умолчанию)
18
18
 
19
- # Создание таблиц в базе данных
19
+ # Импорт структуры данных
20
20
 
21
21
  Возможны два варианта:
22
22
 
23
23
  ```
24
- rake fias:create_tables [DATABASE_URL=... PREFIX=... PATH=...]
24
+ rake fias:create_tables [DATABASE_URL=... PREFIX=... PATH=... ONLY=...]
25
25
  ```
26
26
 
27
27
  Либо:
28
28
 
29
29
  ```
30
- rails g fias:migration [--path=... --prefix=...]
30
+ rails g fias:migration [--path=... --prefix=... --only=...]
31
31
  ```
32
32
 
33
33
  Первый вариант - для использования гема вне рельсов, либо для случая, когда
34
34
  актуальная база ФИАС будет использоваться только для локального мапинга, и
35
35
  на продакшн попасть не должна.
36
36
 
37
+ В параметре only можно передать имена нужных таблиц, houses - все
38
+ таблицы домов.
39
+
37
40
  # Импорт данных
38
41
 
39
42
  ```
40
- rake fias:import:placements fias:import:houses PREFIX=fias PATH=tmp/fias
43
+ rake fias:import PREFIX=fias PATH=tmp/fias ONLY=address_objects
44
+ rake fias:import PREFIX=fias PATH=tmp/fias ONLY=houses
41
45
  ```
42
46
 
43
- Импорт данных разделен на два шага, по скольку, не всегда нужны данные
44
- о строениях, а их почти в 20 раз больше, чем остальных. В большинстве случаев
45
- достаточно городов и улиц.
47
+ Первый пример импортирует только адресные объекты (без справочников),
48
+ второй - только дома.
49
+
50
+ Поддерживается импорт в Postgres и SQLite (нужно для :memory: баз)
51
+
52
+ # Импорт данных в память (для рейк тасок)
53
+
54
+ ```ruby
55
+ ActiveRecord::Base.configurations['fias'] = {
56
+ :adapter => 'sqlite3',
57
+ :database => ':memory:'
58
+ }
59
+ Fias::AddressObject.establish_connection :fias
60
+
61
+ fias = Fias::DbfWrapper.new('tmp/fias')
62
+ importer = Fias::Importer.build(
63
+ adapter: 'sqlite3', connection: Fias::AddressObject.connection.raw_connection
64
+ )
65
+ tables = fias.tables(:address_objects)
66
+
67
+ # Мигрейт. В таком виде так как стандартный мигратор всегда открывает новое
68
+ # соединение с БД, а в случае с SQLite это означает пересоздание базы :memory:.
69
+ Fias::AddressObject.connection.instance_exec do
70
+ eval(importer.schema(tables))
71
+ end
72
+
73
+ importer.import(tables) do |name, record, index|
74
+ record[:aclevel] == 1 # Только активные
75
+ end
76
+
77
+ Fias::AddressObject.count # И дальше импорт
78
+ ```
46
79
 
47
80
  # Некоторые замечания про ФИАС
48
81
 
@@ -55,24 +88,123 @@ rake fias:import:placements fias:import:houses PREFIX=fias PATH=tmp/fias
55
88
  образования хранится в поле AOGUID. Из внешних таблиц логично ссылаться
56
89
  на AOGUID, а не на AOID.
57
90
 
91
+ # Маппинг, он же итерирование
92
+
93
+ Задачи:
94
+
95
+ 1. Сопоставить базу приложения с ФИАСом.
96
+ 2. Уведомлять приложение об изменениях в связанных объектах ФИАСа.
97
+ 3. Уведомлять приложения о новых данных в ФИАСе.
98
+
99
+ В таблице, с которой устанавливаются соответствия нужно создать поле для
100
+ хранения UUID записи ФИАСа.
101
+
102
+ На примере регионов:
103
+
104
+ ```ruby
105
+ # Будем искать соответствия регионов приложения регионам в ФИАС
106
+ scope = Region
107
+ fias_scope = Fias::AddressObject.leveled(:region)
108
+
109
+ # Обработчик событий итератора
110
+ matcher = ->(action, record, *objects) {
111
+ match = objects.first
112
+
113
+ case action
114
+
115
+ # Появилась новая запись в ФИАСе
116
+ when :created
117
+ puts "Region #{match.abbrevated}"
118
+ Region.create!(
119
+ fias_reference: match.id,
120
+ title: match.abbrevated
121
+ )
122
+
123
+ # Элемент в ФИАСе обновлен, нужно (?) обновить базу приложения
124
+ when :updated
125
+ record.update_attributes(
126
+ title: match.abbrevated,
127
+ fias_reference: match.id
128
+ )
129
+ puts "#{record.title} became #{match.abbrevated}"
130
+
131
+ # Объект в ФИАСе был, но из актуального состояния перешел в неактуальное.
132
+ # Типа, Припять: умерший город.
133
+ # Скорее всего, такой элемент в базе приложения нужно скрыть.
134
+ when :deleted
135
+ raise NotImplementedError, "#{record.title} #{objects.first.abbrevated}"
136
+
137
+ # Элемент в ФИАСе разделился: например, поселение Черто-Полохово
138
+ # было преобразовано в две деревни: Чертово и Полохово
139
+ # В этом случае параметр objects содержит объекты ФИАСа, на которые
140
+ # разделился старый.
141
+ when :split
142
+ raise NotImplementedError
143
+
144
+ # Элемент в ФИАСе объединился
145
+ # Типа, присоединили Московскую область к Москве
146
+ # В этом случае objects.first это запись ФИАС о новом объекте, а остальные
147
+ # элементы objects - записи ФИАС объединившихся объектов.
148
+ when :joined
149
+ raise NotImplementedError
150
+
151
+ # Элементу базы приложения нет соответствия в ФИАСе. Пытаемся сопоставить
152
+ # и зафиксировать, если найшли соответствующий вариант.
153
+ when :match
154
+ match = objects.detect { |b| b.actual? || b.name == record.title }
155
+ if match.present?
156
+ record.update_attributes(
157
+ title: match.abbrevated,
158
+ fias_reference: match.id
159
+ )
160
+ puts "#{record.title} became #{match.abbrevated} (new)"
161
+ else
162
+ puts "Record not found #{record.title}"
163
+ end
164
+
165
+ else
166
+ raise 'Unknown action!'
167
+ end
168
+ }
169
+
170
+ # Итерирует существующие в базе приложения элементы
171
+ # fias_reference - название колонки с UUID соответствующей региону записи
172
+ # ФИАСа
173
+ # title - название региона
174
+ fias_scope.match_existing(scope, :fias_reference, :title, &matcher)
175
+
176
+ # Итерирует отсутствующие в базе приложения, но имеющиеся в скоупе
177
+ # ФИАСа адреса.
178
+ fias_scope.actual.match_missing(scope, :fias_reference, &matcher)
179
+ ```
180
+
181
+ Примечания:
182
+
183
+ 1. В случае неоднозначного совпадения ФИАС и приложения, нужно ставить поиск
184
+ соответствия на модерацию.
185
+ 2. В случае :split, :joined - тоже на модерацию. Хотя, разделения происходят
186
+ и нечасто, однако, в ФИАСе таких случаев больше полутора тысяч.
187
+ 3. При начальном импорте данных из ФИАСа, #match_missing лучше начать вызывать
188
+ только после того как соответствия старой базы ФИАСу будут полностью разрулены.
189
+
58
190
  # Работа с данными
59
191
 
60
192
  Существующие регионы:
61
193
 
62
- ```
194
+ ```ruby
63
195
  Fias::AddressObject.actual.leveled(:region).all
64
196
  ```
65
197
 
66
198
  Подчиненные объекты в регионе (области, районы, столица региона):
67
199
 
68
- ```
200
+ ```ruby
69
201
  region = Fias::AddressObject.actual.leveled(:region).first
70
202
  region.children.actual
71
203
  ```
72
204
 
73
205
  # TODO
74
206
 
75
- 1. Индексы.
207
+ 1. Индексы. Индексы. ИНДЕКСЫ.
76
208
 
77
209
  # Полезные ссылки
78
210
 
data/fias.gemspec CHANGED
@@ -21,4 +21,7 @@ Gem::Specification.new do |gem|
21
21
  gem.add_dependency 'rake'
22
22
  gem.add_dependency 'activerecord', '> 3'
23
23
  gem.add_dependency 'progress_bar'
24
+ gem.add_development_dependency 'pg'
25
+ gem.add_development_dependency 'sqlite3'
26
+ gem.add_development_dependency 'hirb'
24
27
  end
data/lib/fias.rb CHANGED
@@ -3,8 +3,11 @@ require 'dbf'
3
3
  require 'active_record'
4
4
 
5
5
  require 'fias/version'
6
- require 'fias/import/dbf_wrapper'
7
- require 'fias/import/pg'
6
+ require 'fias/dbf_wrapper'
7
+ require 'fias/importer'
8
+ require 'fias/importer/base'
9
+ require 'fias/importer/pg'
10
+ require 'fias/importer/sqlite'
8
11
 
9
12
  require 'fias/railtie' if defined?(Rails)
10
13
 
@@ -1,9 +1,18 @@
1
+ # encoding: utf-8
2
+ # TODO: Поделить на файлы
1
3
  module Fias
2
4
  class AddressObject < ActiveRecord::Base
3
- self.table_name = "#{Rails.application.config.fias.prefix}_address_objects"
5
+ # TODO: Тут надо понять как префикс передать
6
+ if defined?(Rails)
7
+ self.table_name = "#{Rails.application.config.fias.prefix}_address_objects"
8
+ else
9
+ self.table_name = "fias_address_objects"
10
+ end
11
+
4
12
  self.primary_key = 'aoid'
5
13
 
6
14
  alias_attribute :name, :formalname
15
+ alias_attribute :id, :aoid
7
16
 
8
17
  # Родительские объекты (Ленобласть для Лодейнопольского района)
9
18
  # Для "проезд 1-й Конной Лахты 2-й" - Санкт-Петербург и Ленинград.
@@ -20,17 +29,25 @@ module Fias
20
29
  primary_key: 'aoguid',
21
30
  foreign_key: 'parentguid'
22
31
 
23
- # Предыдущая историческая версия названия (Ленинград для Питера)
24
- belongs_to :next_version,
32
+ # Предыдущие исторические версии названия (Ленинград для Питера)
33
+ # Может быть несколько, если произошло слияние
34
+ has_many :previous_versions,
25
35
  class_name: 'AddressObject',
26
- primary_key: 'aoid',
27
- foreign_key: 'nextid'
36
+ foreign_key: 'aoid',
37
+ primary_key: 'previd'
28
38
 
29
- # Следующая историческая версия названия (Питер для Ленинграда)
30
- belongs_to :previous_version,
39
+ # Следующая исторические версии названия (Питер для Ленинграда)
40
+ # Может быть несколько, если произошло разделение
41
+ has_many :next_versions,
31
42
  class_name: 'AddressObject',
32
- primary_key: 'aoid',
33
- foreign_key: 'previd'
43
+ foreign_key: 'aoid',
44
+ primary_key: 'nextid'
45
+
46
+ # Полное наименование типа объекта (город, улица)
47
+ belongs_to :address_object_type,
48
+ class_name: '::Fias::AddressObjectType',
49
+ foreign_key: 'shortname',
50
+ primary_key: 'scname'
34
51
 
35
52
  # Актуальные записи (активные в настоящий момент)
36
53
  # Проверено, что livestatus уже достаточен для идентификации
@@ -45,18 +62,33 @@ module Fias
45
62
  }
46
63
 
47
64
  scope :sorted, order('formalname ASC')
65
+ scope :with_types, includes(:address_object_type)
66
+ scope :matching, ->(name) {
67
+ scope = if self.connection.adapter_name
68
+ where(%{ "formalname" @@ ? OR ? @@ "formalname"}, name, name)
69
+ else
70
+ where('formalname LIKE ?', "%#{name}%")
71
+ end
48
72
 
49
- # Полное наименование типа объекта (город, улица)
50
- belongs_to :address_object_type,
51
- class_name: 'Fias::AddressObjectType',
52
- primary_key: 'shortname',
53
- foreign_key: 'scname'
73
+ # Первыми идут центральные и крупные поселения
74
+ scope.order('aolevel ASC, centstatus DESC')
75
+ }
76
+
77
+ scope :same_region, ->(address_object) {
78
+ where(regioncode: address_object.regioncode)
79
+ }
80
+
81
+ # Значимые поселения
82
+ scope :central, where('centstatus > 0')
54
83
 
55
- # Есть ли исторические варианты записи?
56
84
  def has_history?
57
85
  previd.present?
58
86
  end
59
87
 
88
+ def actual?
89
+ livestatus == 1
90
+ end
91
+
60
92
  # Актуальный родитель. Для 1-го проезда 2-й Конной Лахты - только Питер.
61
93
  def parent
62
94
  parents.actual.first
@@ -66,11 +98,134 @@ module Fias
66
98
  AOLEVELS.key(aolevel)
67
99
  end
68
100
 
101
+ # Название с сокращением / полным наименованием города
102
+ # "г. Санкт-Петербург" / "город Санкт-Петербург"
103
+ #
104
+ # Для работы метода требуется загрузить таблицу :address_object_types
105
+ #
106
+ # Параметр - режим:
107
+ # :obviously - очевидный режим". В этом режиме "Дагестан" вместо
108
+ # "Республика Дагестан", "АО" вместо "Автономная область",
109
+ # но всегда "Краснодарский край"
110
+ # :short - дописываются "г." и "респ."
111
+ # :long - дописываются "город" и "Республика"
112
+ #
113
+ # Пример:
114
+ # .abbrevated(:obviously) # Тульская область, Хакасия, Еврейская Аобл.
115
+ # .abbrevated(:short) # Тульская область, Респ. Хакасия, Еврейская Аобл.
116
+ # .abbrevated(:long) # Тульская область, Республика Хакасия, Еврейская автономная область
117
+ def abbrevated(mode = :obviously)
118
+ return name if address_object_type.blank? || shortname.blank?
119
+
120
+ case aolevel_sym
121
+ when :region
122
+ # "Ханты-Мансийский Автономный округ - Югра"
123
+ return name if regioncode == '86'
124
+
125
+ ending = name[-2..-1]
126
+
127
+ # Если название кончается на -ая -ий - по правилам русского языка
128
+ # нужно дописать в конец "область", "край"
129
+ must_append = SHN_MUST_APPEND_TO_ENDINGS.include?(ending) ||
130
+ shortname == 'Чувашия'
131
+
132
+ must_abbrevate = must_append ||
133
+ shortname == 'Чувашия' ||
134
+ mode != :obviously
135
+
136
+ return name unless must_abbrevate
137
+
138
+ abbr = case mode
139
+ when :short
140
+ shortname
141
+ when :long
142
+ address_object_type.name
143
+ when :obviously
144
+ if SHN_LEAVE_SHORTS_INTACT.include?(shortname)
145
+ shortname
146
+ else
147
+ address_object_type.name
148
+ end
149
+ end
150
+
151
+ # Точка не ставится для АО, края, Чувашии и длинных названий
152
+ abbr = "#{abbr}." if mode == :short && not(shortname.in?(SHN_MUST_NOT_APPEND_DOT))
153
+
154
+ if not(must_append) && must_abbrevate
155
+ "#{abbr} #{name}"
156
+ else
157
+ # "Республика" => "республика", но "АО" остается
158
+ abbr = abbr.mb_chars.downcase if not(shortname.in?(SHN_LEAVE_SHORTS_INTACT))
159
+ "#{name} #{abbr}"
160
+ end
161
+ else
162
+ abbr = if mode == :long
163
+ address_object_type.try(:name)
164
+ else
165
+ shortname
166
+ end
167
+ "#{abbr} #{name}"
168
+ end
169
+ end
170
+
171
+ class << self
172
+ # Подробное описание см. в README
173
+ # TODO: OPERSTATUS
174
+ def match_existing(scope, fias_key_accessor, title_field_accessor, &block)
175
+ scope.find_each do |record|
176
+ aoid = record.send(fias_key_accessor)
177
+ title = record.send(title_field_accessor)
178
+
179
+ if aoid.present?
180
+ match = scoped.find_by_aoid(aoid)
181
+
182
+ if match.present?
183
+ unless match.actual?
184
+ next_versions = match.next_versions
185
+
186
+ if next_versions.empty?
187
+ yield(:deleted, record, match)
188
+ elsif next_versions.count == 1
189
+ next_version = next_versions.first
190
+ previous_versions_of_current = next_version.previous_versions
191
+
192
+ if previous_versions_of_current.count == 1
193
+ yield(:updated, record, next_version)
194
+ else
195
+ yield(:joined, record, next_version, previous_versions_of_current)
196
+ end
197
+ elsif next_versions.count > 1
198
+ yield(:split, record, next_versions)
199
+ end
200
+ end
201
+ end
202
+ else
203
+ matches = scoped.matching(title)
204
+ yield(:match, record, *matches)
205
+ end
206
+ end
207
+ end
208
+
209
+ def match_missing(scope, address_object_key_field, &block)
210
+ scoped.each do |address_object|
211
+ unless scope.where(address_object_key_field => address_object.aoid).exists?
212
+ yield(:created, nil, address_object)
213
+ end
214
+ end
215
+ end
216
+ end
217
+
69
218
  # Коды уровня адресного объекта
70
219
  AOLEVELS = {
71
220
  region: 1, autonomy: 2, district: 3, city: 4,
72
221
  territory: 5, settlement: 6, street: 7,
73
222
  additional_territory: 90, additional_territory_slave: 91
74
223
  }
224
+
225
+ # Дописывать сокращения обязательно, "Самарская" выглядит странно,
226
+ # всегда должно быть "Самарская область", а "Дагестан" понятно и так.
227
+ SHN_MUST_APPEND_TO_ENDINGS = %w(ая ий)
228
+ SHN_MUST_NOT_APPEND_DOT = %w(край АО Чувашия) # Не дописывать точку к сокращению
229
+ SHN_LEAVE_SHORTS_INTACT = %w(АО Аобл Чувашия) # В очевидном режиме не разворачивать "АО" в "Автономная область"
75
230
  end
76
231
  end
@@ -1,8 +1,15 @@
1
+ # encoding: utf-8
1
2
  module Fias
2
3
  class AddressObjectType < ActiveRecord::Base
3
- self.table_name = "#{Rails.application.config.fias.prefix}_fias_address_object_types"
4
+ # TODO: Тут надо понять как префикс передать
5
+ if defined?(Rails)
6
+ self.table_name = "#{Rails.application.config.fias.prefix}_address_object_types"
7
+ else
8
+ self.table_name = "fias_address_object_types"
9
+ end
4
10
  self.primary_key = 'scname'
5
11
 
6
12
  alias_attribute :name, :socrname
13
+ alias_attribute :abbrevation, :scname
7
14
  end
8
15
  end
@@ -0,0 +1,90 @@
1
+ module Fias
2
+ # Класс для доступа к DBF-файлам ФИАС.
3
+ #
4
+ # Пример:
5
+ # wrapper = Fias::DbfWrapper.new('tmp/fias')
6
+ # wrapper.address_objects.record_count
7
+ # wrapper.address_objects.each { |record| record.attributes }
8
+ #
9
+ # TODO: Добавить в инишилайзер tables, чтобы при создании проверять
10
+ # их наличие на диске
11
+ class DbfWrapper
12
+ # Открывает DBF-файлы ФИАСа
13
+ def initialize(pathspec)
14
+ unless Dir.exists?(pathspec)
15
+ raise ArgumentError, 'FIAS database path does not exists'
16
+ end
17
+ self.pathspec = pathspec
18
+
19
+ DBF_ACCESSORS.each do |accessor, dbf_name|
20
+ filename = File.join(pathspec, dbf_name)
21
+
22
+ if File.exists?(filename)
23
+ dbf = DBF::Table.new(filename, nil, DEFAULT_ENCODING)
24
+ send("#{accessor}=", dbf)
25
+ end
26
+ end
27
+ end
28
+
29
+ # Возвращает хеш {аксессор dbf-таблицы => таблица}
30
+ # На входе массив названий таблиц (строки), без параметров - все
31
+ # Макрос houses вернет все таблицы домов
32
+ #
33
+ # Пример:
34
+ # wrapper = Fias::DbfWrapper.new('tmp/fias')
35
+ # wrapper.tables(%w(address_objects address_object_types houses))
36
+ # ... {
37
+ # ... address_objects: Dbf::Table(...),
38
+ # ... address_object_types: Dbf::Table(...),
39
+ # ... house1: Dbf::Table(...),
40
+ # ... ...
41
+ # ... house99: Dbf::Table(...)
42
+ # ... }
43
+ def tables(*only)
44
+ only = only.first if only.first.is_a?(Array)
45
+ only = only.map(&:to_s)
46
+
47
+ hash = DBF_ACCESSORS.keys.map do |accessor|
48
+ accessor_s = accessor.to_s
49
+ is_houses = only.include?('houses') && accessor_s.starts_with?('house')
50
+
51
+ if only.include?(accessor_s) || is_houses
52
+ [accessor, send(accessor)]
53
+ end
54
+ end
55
+ Hash[*hash.compact.flatten]
56
+ end
57
+
58
+ # { house01: "HOUSE01"..house99: "HOUSE99" }
59
+ HOUSES_ACCESSORS = Hash[*(1..99).map { |n|
60
+ [("house%0.2d" % n).to_sym, "HOUSE%0.2d.dbf" % n]
61
+ }.flatten]
62
+
63
+ # Таблица соответствий аттрибутов класса DBF-файлам
64
+ DBF_ACCESSORS = {
65
+ address_object_types: 'SOCRBASE.DBF',
66
+ current_statuses: 'CURENTST.DBF',
67
+ actual_statuses: 'ACTSTAT.DBF',
68
+ operation_statuses: 'OPERSTAT.DBF',
69
+ center_statuses: 'CENTERST.DBF',
70
+ interval_statuses: 'INTVSTAT.DBF',
71
+ estate_statues: 'ESTSTAT.DBF',
72
+ structure_statuses: 'STRSTAT.DBF',
73
+ address_objects: 'ADDROBJ.DBF',
74
+ house_intervals: 'HOUSEINT.DBF',
75
+ landmarks: 'LANDMARK.DBF'
76
+ }.merge(
77
+ HOUSES_ACCESSORS
78
+ )
79
+
80
+ DEFAULT_ENCODING = Encoding::CP866
81
+
82
+ attr_reader *DBF_ACCESSORS.keys
83
+ attr_reader :houses
84
+
85
+ private
86
+ attr_accessor :pathspec
87
+ attr_writer *DBF_ACCESSORS.keys
88
+ attr_writer :houses
89
+ end
90
+ end
@@ -0,0 +1,30 @@
1
+ module Fias
2
+ module Importer
3
+ # Фактори-метод, возвращает ссылку на объект импортера
4
+ # Принимает параметры:
5
+ # :adapter - название адаптера
6
+ # :connection - прямое соединение с базой данных (connection.raw_connection)
7
+ def self.build(options = {})
8
+ adapter = options.delete(:adapter) ||
9
+ ActiveRecord::Base.connection_config[:adapter]
10
+
11
+ connection = options.delete(:connection) ||
12
+ ActiveRecord::Base.connection.raw_connection
13
+
14
+ case adapter
15
+ when 'postgresql'
16
+ Pg.new(
17
+ connection,
18
+ options
19
+ )
20
+ when 'sqlite3'
21
+ Sqlite.new(
22
+ connection,
23
+ options
24
+ )
25
+ else
26
+ raise 'Only postgres & sqlite supported now, fork'
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,59 @@
1
+ module Fias
2
+ module Importer
3
+ class Base
4
+ def initialize(connection, options = {})
5
+ self.prefix = options.delete(:prefix) || 'fias'
6
+ self.connection = connection
7
+ end
8
+
9
+ # На входе - хеш (имя таблицы => dbf)
10
+ def schema(tables)
11
+ "".tap do |s|
12
+ tables.each do |name, dbf|
13
+ if dbf.present?
14
+ s << schema_for(name, table_name(name), dbf)
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ def schema_for(name, table_name, dbf)
21
+ "".tap do |s|
22
+ s << %{create_table "#{table_name}", id: false do |t|\n}
23
+ s << schema_columns(name, dbf)
24
+ s << "end\n"
25
+ end
26
+ end
27
+
28
+ def import(tables, &block)
29
+ tables.each do |name, dbf|
30
+ if dbf
31
+ import_table(name, table_name(name), dbf, &block)
32
+ end
33
+ end
34
+ end
35
+
36
+ def import_table(name, table_name, dbf, &block)
37
+ raise NotImplementedError, 'Implement this in concrete class'
38
+ end
39
+
40
+ protected
41
+ attr_accessor :prefix, :connection
42
+
43
+ private
44
+ def table_name(name)
45
+ "#{prefix}_#{name}"
46
+ end
47
+
48
+ def schema_columns(accessor, table)
49
+ "".tap do |s|
50
+ table.columns.each do |column|
51
+ column_name = column.name.downcase
52
+ column_def = column.schema_definition
53
+ s << " t.column #{column_def}"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,81 @@
1
+ module Fias
2
+ module Importer
3
+ # Класс для импорта данных из ФИАС в PostgreSQL.
4
+ # Используется COPY FROM STDIN.
5
+ class Pg < Base
6
+ def schema_for(name, table_name, dbf)
7
+ super + alter_table_to_pg_uuids(name, table_name)
8
+ end
9
+
10
+ private
11
+ def alter_table_to_pg_uuids(name, table_name)
12
+ "".tap do |s|
13
+ columns = CONVERT_TO_UUID[name]
14
+ if columns.present?
15
+ columns.each do |column|
16
+ s << %{ActiveRecord::Base.connection.execute("ALTER TABLE #{table_name} ALTER COLUMN #{column} TYPE UUID USING CAST (#{column} AS UUID);")\n}
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ def import_table(name, table_name, dbf, &block)
23
+ fields = table_fields(dbf)
24
+ columns = table_columns(fields)
25
+
26
+ truncate_table(table_name)
27
+ copy_from_stdin(table_name, columns)
28
+
29
+ dbf.each_with_index do |record, index|
30
+ should_import = yield(name, record.attributes, index) if block_given?
31
+
32
+ unless should_import === false
33
+ data = record.to_a
34
+ put_data(data)
35
+ end
36
+ end
37
+
38
+ put_copy_end
39
+ end
40
+
41
+ def truncate_table(table_name)
42
+ connection.exec "TRUNCATE TABLE #{table_name};"
43
+ end
44
+
45
+ def table_fields(table)
46
+ table.columns.map(&:name).map(&:downcase)
47
+ end
48
+
49
+ def table_columns(fields)
50
+ fields.join(', ')
51
+ end
52
+
53
+ def copy_from_stdin(table_name, columns)
54
+ sql = "COPY #{table_name} (#{columns}) FROM STDIN NULL AS '-nil-'\n"
55
+ connection.exec(sql)
56
+ end
57
+
58
+ def put_data(data)
59
+ data.map! { |item| item == "" ? '-nil-' : item }
60
+ line = data.join("\t") + "\n"
61
+ connection.put_copy_data(line)
62
+ end
63
+
64
+ def put_copy_end
65
+ connection.put_copy_end
66
+
67
+ while res = connection.get_result
68
+ result_status = res.res_status(res.result_status)
69
+ unless result_status == 'PGRES_COMMAND_OK'
70
+ raise "Import failure: #{result_status}"
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ # Эти поля нужно отконвертировать в тип UUID после создания
77
+ CONVERT_TO_UUID = {
78
+ address_objects: %w(aoguid aoid previd nextid parentguid)
79
+ }
80
+ end
81
+ end
@@ -0,0 +1,38 @@
1
+ module Fias
2
+ module Importer
3
+ # Нужно для :memory: баз в первую очередь
4
+ class Sqlite < Base
5
+ def import_table(name, table_name, dbf, &block)
6
+ truncate_table(table_name)
7
+
8
+ qmarks = ['?'] * dbf.columns.keys.size
9
+ qmarks = qmarks.join(', ')
10
+
11
+ dbf.each_with_index do |record, index|
12
+ data = record.attributes
13
+
14
+ should_import = yield(name, data, index) if block_given?
15
+
16
+ unless should_import === false
17
+ columns = data.keys.join(', ')
18
+
19
+ values = data.values.map do |value|
20
+ if value.is_a?(Date)
21
+ value.to_s
22
+ else
23
+ value
24
+ end
25
+ end
26
+
27
+ connection.execute("INSERT INTO #{table_name} (#{columns}) VALUES (#{qmarks});", values)
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+ def truncate_table(table_name)
34
+ connection.execute "DELETE FROM #{table_name} WHERE 1"
35
+ end
36
+ end
37
+ end
38
+ end
data/lib/fias/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Fias
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -4,14 +4,18 @@ module Fias
4
4
 
5
5
  class_option :prefix, type: :string, default: :fias, desc: 'Table names prefix'
6
6
  class_option :path, type: :string, default: 'tmp/fias', desc: 'Path to FIAS dbfs'
7
+ class_option :only, type: :string, default: '', desc: 'Only tables'
7
8
 
8
9
  source_root File.expand_path("../templates", __FILE__)
9
10
 
10
11
  def generate_migration
11
- wrapper = Fias::Import::DbfWrapper.new(options.path)
12
- importer = wrapper.build_importer(prefix: options.prefix)
13
- @schema = importer.schema
14
- @schema.gsub!("\n", "\n\t\t")
12
+ only = options.only.split(',').map(&:strip)
13
+ wrapper = Fias::DbfWrapper.new(options.path)
14
+ importer = Fias::Importer.build(prefix: options.prefix)
15
+
16
+ tables = wrapper.tables(only)
17
+ @schema = importer.schema(tables)
18
+ @schema.gsub!("\n", "\n ")
15
19
 
16
20
  migration_template 'create_fias_tables.rb', 'db/migrate/create_fias_tables'
17
21
  end
data/tasks/fias.rake CHANGED
@@ -25,46 +25,43 @@ namespace :fias do
25
25
  end
26
26
 
27
27
  fias_path = ENV['FIAS'] || 'tmp/fias'
28
- wrapper = Fias::Import::DbfWrapper.new(fias_path)
29
- importer = wrapper.build_importer(prefix: ENV['PREFIX'])
28
+ wrapper = Fias::DbfWrapper.new(fias_path)
29
+ importer = Fias::Importer.build(prefix: ENV['PREFIX'])
30
30
 
31
31
  yield(wrapper, importer)
32
32
  end
33
+
34
+ def only
35
+ only = ENV['ONLY'].to_s.split(',').map(&:strip)
36
+ end
33
37
  end
34
38
 
35
- desc 'Create FIAS tables (could specify tables PREFIX, PATH to dbfs and DATABASE_URL)'
39
+ desc 'Create FIAS tables (could specify tables PREFIX, PATH to dbfs and DATABASE_URL, EXCLUDE or ONLY tables)'
36
40
  task may_be_rails(:create_tables) do
37
41
  within_connection do |wrapper, importer|
38
- ActiveRecord::Schema.define { eval(importer.schema) }
42
+ tables = wrapper.tables(only)
43
+ # TODO: Добавить во враппер tables, это убрать
44
+ raise "DBF file not found for: #{key}" if tables.keys.any? { |key| key.nil? }
45
+ ActiveRecord::Schema.define do
46
+ eval(importer.schema(tables))
47
+ end
39
48
  end
40
49
  end
41
50
 
42
- namespace :import do
43
- desc 'Import FIAS data (without houses)'
44
- task may_be_rails(:placements) do
45
- within_connection do |wrapper, importer|
46
- total_record_count = wrapper.tables.sum { |accessor, dbf| dbf.record_count }
47
-
48
- puts 'Importing FIAS data...'
51
+ desc 'Import FIAS data'
52
+ task may_be_rails(:import) do
53
+ within_connection do |wrapper, importer|
54
+ tables = wrapper.tables(only)
49
55
 
50
- bar = ProgressBar.new(total_record_count)
51
- importer.import do
52
- bar.increment!
53
- end
56
+ total_record_count = tables.sum do |accessor, dbf|
57
+ dbf.present? ? dbf.record_count : 0
54
58
  end
55
- end
56
59
 
57
- desc 'Import FIAS data (houses)'
58
- task may_be_rails(:houses) do
59
- within_connection do |wrapper, importer|
60
- total_record_count = wrapper.houses.sum { |region, dbf| dbf.record_count }
60
+ puts 'Importing FIAS data...'
61
61
 
62
- puts 'Importing FIAS data (houses)...'
63
-
64
- bar = ProgressBar.new(total_record_count)
65
- importer.import_houses do
66
- bar.increment!
67
- end
62
+ bar = ProgressBar.new(total_record_count)
63
+ importer.import(tables) do
64
+ bar.increment!
68
65
  end
69
66
  end
70
67
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fias
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-21 00:00:00.000000000 Z
12
+ date: 2013-01-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: dbf
@@ -75,6 +75,54 @@ dependencies:
75
75
  - - ! '>='
76
76
  - !ruby/object:Gem::Version
77
77
  version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: pg
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: sqlite3
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: hirb
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
78
126
  description: Ruby wrapper to FIAS database
79
127
  email:
80
128
  - gzigzigzeo@evilmartians.com
@@ -91,8 +139,11 @@ files:
91
139
  - lib/fias.rb
92
140
  - lib/fias/active_record/address_object.rb
93
141
  - lib/fias/active_record/address_object_type.rb
94
- - lib/fias/import/dbf_wrapper.rb
95
- - lib/fias/import/pg.rb
142
+ - lib/fias/dbf_wrapper.rb
143
+ - lib/fias/importer.rb
144
+ - lib/fias/importer/base.rb
145
+ - lib/fias/importer/pg.rb
146
+ - lib/fias/importer/sqlite.rb
96
147
  - lib/fias/railtie.rb
97
148
  - lib/fias/version.rb
98
149
  - lib/generators/fias/migration.rb
@@ -112,7 +163,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
112
163
  version: '0'
113
164
  segments:
114
165
  - 0
115
- hash: -2160443642439180378
166
+ hash: 493497803985021140
116
167
  required_rubygems_version: !ruby/object:Gem::Requirement
117
168
  none: false
118
169
  requirements:
@@ -121,7 +172,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
121
172
  version: '0'
122
173
  segments:
123
174
  - 0
124
- hash: -2160443642439180378
175
+ hash: 493497803985021140
125
176
  requirements: []
126
177
  rubyforge_project:
127
178
  rubygems_version: 1.8.24
@@ -1,83 +0,0 @@
1
- module Fias
2
- module Import
3
- # Класс для доступа к DBF-файлам ФИАС
4
- # Пример:
5
- # wrapper = DbfWrapper.new('tmp/fias')
6
- # wrapper.address_objects.record_count
7
- # wrapper.address_objects.each { |record| record.attributes }
8
- class DbfWrapper
9
- def initialize(pathspec)
10
- unless Dir.exists?(pathspec)
11
- raise ArgumentError, 'FIAS database path does not exists'
12
- end
13
- self.pathspec = pathspec
14
-
15
- TABLES_ACCESSORS.each do |key, dbf_name|
16
- filename = File.join(pathspec, dbf_name)
17
- dbf = DBF::Table.new(filename, nil, DEFAULT_ENCODING)
18
-
19
- send("#{key}=", dbf)
20
- end
21
-
22
- self.houses = {}
23
- Dir[File.join(pathspec, HOUSE_DBF_MASK)].each do |filename|
24
- File.basename(filename) =~ /(\d+)/
25
- region_code = $1.to_i
26
-
27
- dbf = DBF::Table.new(filename, nil, DEFAULT_ENCODING)
28
- self.houses[region_code] = dbf
29
- end
30
- end
31
-
32
- def tables
33
- hash = TABLES_ACCESSORS.keys.map do |accessor|
34
- [accessor, send(accessor)]
35
- end
36
- end
37
-
38
- def build_importer(options)
39
- config = ActiveRecord::Base.connection_config
40
- if config[:adapter] == 'postgresql'
41
- import = Fias::Import::PgImporter.new(
42
- ActiveRecord::Base.connection.raw_connection,
43
- self,
44
- options
45
- )
46
- else
47
- raise 'Only postgres supported now, fork'
48
- end
49
- end
50
-
51
- # Таблица соответствий аттрибутов класса DBF-файлам
52
- TABLES_ACCESSORS = {
53
- address_object_types: 'SOCRBASE.DBF',
54
- current_statuses: 'CURENTST.DBF',
55
- actual_statuses: 'ACTSTAT.DBF',
56
- operation_statuses: 'OPERSTAT.DBF',
57
- center_statuses: 'CENTERST.DBF',
58
- interval_statuses: 'INTVSTAT.DBF',
59
- estate_statues: 'ESTSTAT.DBF',
60
- structure_statuses: 'STRSTAT.DBF',
61
- address_objects: 'ADDROBJ.DBF',
62
- house_intervals: 'HOUSEINT.DBF',
63
- landmarks: 'LANDMARK.DBF'
64
- }
65
- HOUSE_DBF_MASK = 'HOUSE??.DBF'
66
-
67
- # Эти поля нужно отконвертировать в тип UUID после создания
68
- CONVERT_TO_UUID = {
69
- address_objects: %w(aoguid aoid previd nextid parentguid)
70
- }
71
-
72
- DEFAULT_ENCODING = Encoding::CP866
73
-
74
- attr_reader *TABLES_ACCESSORS.keys
75
- attr_reader :houses
76
-
77
- private
78
- attr_accessor :pathspec
79
- attr_writer *TABLES_ACCESSORS.keys
80
- attr_writer :houses
81
- end
82
- end
83
- end
@@ -1,144 +0,0 @@
1
- module Fias
2
- module Import
3
- class PgImporter
4
- # Класс для импорта данных из ФИАС в PostgreSQL.
5
- # Используется COPY FROM STDIN.
6
- def initialize(raw_connection, wrapper, options = {})
7
- self.prefix = options.delete(:prefix) || 'fias'
8
- self.raw_connection = raw_connection
9
- self.wrapper = wrapper
10
- end
11
-
12
- # Генерирует схему базы для ActiveRecord в виде строки
13
- def schema
14
- "".tap do |s|
15
- wrapper.tables.each do |accessor, table|
16
- s << %{create_table "#{table_name(accessor)}", id: false do |t|\n}
17
- s << dump_schema_for(accessor, table)
18
- s << "end\n"
19
-
20
- s << alter_table_to_pg_uuids(accessor)
21
- end
22
-
23
- s << %{create_table "#{table_name('houses')}", id: false do |t|\n}
24
- s << dump_schema_for(:houses, wrapper.houses.values.first)
25
- s << " t.column :regioncode, :integer\n"
26
- s << "end\n"
27
- end
28
- end
29
-
30
- def import(&block)
31
- wrapper.tables.each do |accessor, table|
32
- import_table(accessor, table, &block)
33
- end
34
- end
35
-
36
- def import_houses(&block)
37
- wrapper.houses.each do |region, table|
38
- import_houses_for_region(region, table, &block)
39
- end
40
- end
41
-
42
- private
43
- def dump_schema_for(accessor, table)
44
- "".tap do |s|
45
- table.columns.each do |column|
46
- column_name = column.name.downcase
47
- column_def = column.schema_definition
48
- s << " t.column #{column_def}"
49
- end
50
- end
51
- end
52
-
53
- def alter_table_to_pg_uuids(accessor)
54
- "".tap do |s|
55
- columns = wrapper.class::CONVERT_TO_UUID[accessor]
56
- if columns.present?
57
- columns.each do |column|
58
- s << %{ActiveRecord::Base.connection.execute("ALTER TABLE #{table_name(accessor)} ALTER COLUMN #{column} TYPE UUID USING CAST (#{column} AS UUID);")\n}
59
- end
60
- end
61
- end
62
- end
63
-
64
- def import_table(accessor, table, &block)
65
- fields = table_fields(table)
66
- columns = table_columns(fields)
67
-
68
- tn = table_name(accessor)
69
-
70
- truncate_table(tn)
71
- copy_from_stdin(tn, columns)
72
-
73
- table.each_with_index do |record|
74
- data = record.to_a
75
- put_data(data)
76
- yield(accessor, data) if block_given?
77
- end
78
-
79
- put_copy_end
80
- end
81
-
82
- def import_houses_for_region(region, table, &block)
83
- fields = table_fields(table)
84
- fields << :regioncode
85
- columns = table_columns(fields)
86
-
87
- tn = table_name('houses')
88
-
89
- truncate_table(tn)
90
- copy_from_stdin(tn, columns)
91
-
92
- table.each_with_index do |record|
93
- data = record.to_a
94
- data << region
95
- put_data(data)
96
- yield('houses', data) if block_given?
97
- end
98
-
99
- put_copy_end
100
- end
101
-
102
- def table_name(table)
103
- "#{prefix}_#{table}"
104
- end
105
-
106
- def table_fields(table)
107
- table.columns.map(&:name).map(&:downcase)
108
- end
109
-
110
- def table_columns(fields)
111
- fields.join(',')
112
- end
113
-
114
- def truncate_table(table_name)
115
- raw_connection.exec "TRUNCATE TABLE #{table_name};"
116
- end
117
-
118
- def copy_from_stdin(table_name, columns)
119
- sql = "COPY #{table_name} (#{columns}) FROM STDIN NULL AS '-nil-'\n"
120
- raw_connection.exec(sql)
121
- end
122
-
123
- def put_data(data)
124
- data.map! { |item| item == "" ? '-nil-' : item }
125
- line = data.join("\t") + "\n"
126
- raw_connection.put_copy_data(line)
127
- end
128
-
129
- def put_copy_end
130
- raw_connection.put_copy_end
131
-
132
- while res = raw_connection.get_result
133
- result_status = res.res_status(res.result_status)
134
- unless result_status == 'PGRES_COMMAND_OK'
135
- raise "Import failure: #{result_status}"
136
- end
137
- end
138
- end
139
-
140
- attr_accessor :raw_connection, :wrapper
141
- attr_accessor :prefix, :table_mappings
142
- end
143
- end
144
- end