Compare commits

..

31 Commits
py2 ... master

Author SHA1 Message Date
Georg K
3b47bfd3a3 feat: add route to convert aoid2aoguid 2020-12-24 04:51:59 +03:00
jar3b
966bec093f refactor: remove unused python2 code, optimize imports 2020-02-19 01:29:08 +03:00
jar3b
d03aa0a78c debug: remove logging for '__get_suggest' 2020-02-19 00:45:22 +03:00
jar3b
67cfd34a66 debug: fix logging for '__get_suggest' 2020-02-19 00:42:20 +03:00
jar3b
03d432cb25 debug: add logging for '__get_suggest' 2020-02-19 00:41:08 +03:00
jar3b
63815b0871 fix: "TypeError: reading file objects must return bytes objects" while creating DB from directory (after lxml update) 2020-02-02 02:05:05 +03:00
jar3b
9d4928a9e5 feat: update license (year) and .gitingore 2020-02-02 02:04:26 +03:00
jar3b
ec6fc6a8c0 Update configs, fix soap url 2018-11-07 18:49:44 +03:00
jar3b
1452debf94 Update requirements.txt 2018-11-07 18:37:59 +03:00
jar3b
56ba93b1b7 Update readme and requirements.txt 2018-11-07 18:35:21 +03:00
Jack Stdin
85e1ecfbaa Add codecs.open instad of open (for DB fetcher on Win7) 2017-01-25 12:05:29 +03:00
jar3b
f2659b0010 Add multiple error route 2016-09-22 12:22:12 +03:00
jar3b
e11a9811c9 Updating readme 2016-09-22 12:19:36 +03:00
jar3b
f688e0c411 Fix requirements and readme 2016-09-22 11:45:27 +03:00
jar3b
79d14ce13e Switch to bottle-oop-rest lib supports gevent 2016-09-22 10:55:43 +03:00
jar3b
afd61dae19 Add "OPTIONS" handling for more restful responses 2016-09-21 17:19:45 +03:00
jar3b
2f5df29d0a Add systemd config for gunicorn (description in readme) 2016-06-26 15:19:46 +03:00
jar3b
cf74075b00 Added end-processed check on DB create 2016-06-26 14:48:22 +03:00
jar3b
f079a1edbb Fix manage.py version result 2016-06-26 14:29:47 +03:00
jar3b
eeebf5f8d3 Update manage to serve DB (create with spec version) 2016-06-26 14:21:11 +03:00
jar3b
d4668833dc Updated readme 2016-04-25 12:23:08 +03:00
jar3b
7a2ba33d49 Updated readme 2016-04-25 12:16:16 +03:00
jar3b
67f64b7aec Add gitkeep 2016-04-25 11:51:02 +03:00
jar3b
893d1576e3 Readme updated 2016-04-25 11:47:08 +03:00
jar3b
552a6355c7 Add gunicorn config 2016-04-25 11:09:22 +03:00
jar3b
3e6ccc96a1 Renamed wsgi module 2016-04-25 11:06:49 +03:00
jar3b
fc3d2f5e64 Updated README.md 2016-04-24 13:17:05 +03:00
jar3b
27efa759ef Updated README.md and config (slashes for unix socket) 2016-04-24 12:58:50 +03:00
jar3b
72d0036879 Some API fix:
now /find/<text>?strong=<0,1> is right route,
'find' always returns array
2016-04-24 12:31:18 +03:00
jar3b
682200c4c1 Readme updated 2016-04-22 16:45:50 +03:00
jar3b
c7f0de74a5 Big commit for py3 2016-04-22 14:30:05 +03:00
32 changed files with 393 additions and 268 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
.idea/
*.log
config.py

View File

@ -1,4 +1,4 @@
Copyright (c) 2016, hellotan
Copyright (c) 2020, jar3b
All rights reserved.
Redistribution and use in source and binary forms, with or without

251
README.md
View File

@ -1,11 +1,14 @@
# py-phias
Python application that can operate with FIAS (Russian Address Object DB)
Простое приложение для работы с БД ФИАС, написано для Python 2.7, использует БД PostgreSQL
Простое приложение для работы с БД ФИАС, написано для Python 3, использует БД PostgreSQL
## Содержание
- [Возможности](#Возможности)
- [Установка](#Установка)
- [Настройка](#Настройка)
- [API](#Api)
## Возможности
1. API (выходной формат - JSON), основные функции которого:
@ -22,12 +25,14 @@ Python application that can operate with FIAS (Russian Address Object DB)
## Установка
Протестирована работа на следующих ОС: [Windows](#windows) (8.1) и [Debian](#debian-linux) Jessie
Протестирована работа на следующих ОС: [Windows](#windows) (8.1, 10) и [Debian](#debian-linux) Jessie, Stretch.
Необходима версия Python == 3.5
### Зависимости
_Внимание_! Только Python 2.7+ (на 3+ не тестировал), только PostgreSQL, только Sphinx. MySQL/MariaDB, ElasticSearch/Solr
не поддерживаются и, скорее всего, не будут.
_Внимание_! Только Python 3 (для 2.7 пока есть отдельная ветка), только PostgreSQL, только Sphinx. MySQL/MariaDB,
ElasticSearch/Solr не поддерживаются и, скорее всего, не будут.
Для работы приложения необходимо достаточное кол-во RAM (1Gb+) и ~5.5Gb места на диске
(3-3.5Gb для скачивания архива с базой, 350-400Mb для индексов Sphinx, 1Gb для базы). Также необходимы root права
@ -38,33 +43,38 @@ _Внимание_! Только Python 2.7+ (на 3+ не тестировал)
Предварительно обязательно установить и настроить:
1. Python 2.7.x, pip
1. Python 3, pip
Для Windows качаем - ставим, для Debian:
```
sudo apt-get install python-setuptools
sudo easy_install pip
sudo pip install --upgrade pip
sudo apt-get install python3-setuptools
sudo easy_install3 pip
sudo pip3 install --upgrade pip
```
2. PostgreSql 9.5 и выше (из-за синтаксиса _ON CONFLICT ... DO_)
Для Windows, как обычно, [качаем](http://www.enterprisedb.com/products-services-training/pgdownload#windows) - ставим, для Debian:
Для Windows, как обычно, [качаем](http://www.enterprisedb.com/products-services-training/pgdownload#windows) - ставим,
для Debian 8 и ниже:
```
sudo sh -c 'echo deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main 9.5 > /etc/apt/sources.list.d/postgresql.list'
sudo sh -c 'echo deb http://apt.postgresql.org/pub/repos/apt/ jessie-pgdg main 9.5 > /etc/apt/sources.list.d/postgresql.list'
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt-get update
sudo apt-get install postgresql-9.5
```
, для Debian 9:
`sudo apt-get install postgresql`
Затем создайте пользователя и базу данных и установите расширение pg_trgm:
```
sudo adduser phias
sudo adduser --no-create-home fias
sudo -u postgres psql
postgres=# CREATE DATABASE fias_db;
postgres=# CREATE USER phias WITH password 'phias';
postgres=# GRANT ALL privileges ON DATABASE fias_db TO phias;
postgres=# ALTER USER phias WITH SUPERUSER;
postgres=# CREATE USER fias WITH password 'fias';
postgres=# GRANT ALL privileges ON DATABASE fias_db TO fias;
postgres=# ALTER USER fias WITH SUPERUSER;
postgres=# \q
sudo -u phias psql -d fias_db -U phias
sudo -u fias psql -d fias_db -U fias
postgres=# CREATE EXTENSION pg_trgm SCHEMA public;
postgres=# \q
```
3. Sphinx 2.2.1 и новее:
@ -79,69 +89,66 @@ _Внимание_! Только Python 2.7+ (на 3+ не тестировал)
make
sudo make install
```
, не забудте установить _build-essential_ перед этим (касается Debian).
4. Web-сервер с поддержкой WSGI, любой, по Вашему желанию.
### Windows
1. Установить lxml, скачав [отсюда](https://pypi.python.org/pypi/lxml/3.5.0).
2. Установить unrar.exe (можно установить WinRar целиком).
3. Установить sphinxapi последней версии (либо взять из директории Sphinx):
1. Установить lxml, скачав whl [отсюда](http://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml) и сделав
`pip install yourmodulename.whl`.
2. Есть некоторые проблемы с установкой и работой psycopg2 (Windows 10, VS 2015), если у Вас они присутствуют - качаем
[сборку для Windows](http://www.stickpeople.com/projects/python/win-psycopg/)
3. Если есть проблемы с Python-Levenshtein, скачать [отсюда](http://www.lfd.uci.edu/~gohlke/pythonlibs/#python-levenshtein)
и установить
4. Установить unrar.exe (можно установить WinRar целиком).
5. Установить sphinxapi с поддержкой синтаксиса Python3:
```
python -m pip install https://github.com/Romamo/sphinxapi/zipball/master
pip install https://github.com/jar3b/sphinx-py3-api/zipball/master
```
4. Установить приложение, скачав релиз `https://github.com/jar3b/py-phias/archive/v0.0.2.zip`, распакуйте его в удобное
Вам место и запустите оттуда `python -m pip install -r requirements.txt`
### Debian Linux
1. Установить libxml:
1. Установить unrar (non-free):
```
sudo apt-get install python-dev libxml2 libxml2-dev libxslt-dev
```
2. Установить unrar (non-free):
```
sudo sh -c 'echo deb ftp://ftp.us.debian.org/debian/ stable main non-free > /etc/apt/sources.list.d/non-free.list'
sudo sh -c 'echo deb ftp://ftp.us.debian.org/debian/ <deb_name> main non-free > /etc/apt/sources.list.d/non-free.list'
sudo apt-get update
sudo apt-get install unrar
```
3. Установить sphinxapi последней версии:
2. Устанавливаем и настраиваем libxml:
```
sudo apt-get install libxml2-dev libxslt1-dev python3-dev python3-lxml
```
3. Увстановить python3-dev, для того, чтобы корректно установился levenshtein
```
sudo apt-get install python3-dev
```
### Общая часть:
1. Установим приложение из репозитория:
```
sudo pip install https://github.com/Romamo/sphinxapi/zipball/master
```
4. Установить, собственно, приложение:
- полностью:
```
sudo mkdir -p /var/www/py-phias
sudo chown phias: /var/www/py-phias
wget https://github.com/jar3b/py-phias/archive/v0.0.2.tar.gz
sudo -u phias tar xzf v0.0.1.tar.gz -C /var/www/py-phias --strip-components=1
cd /var/www/py-phias
sudo pip install -r requirements.txt
```
- как repo:
```
sudo mkdir -p /var/www/py-phias
sudo chown phias: /var/www/py-phias
cd /var/www
sudo -u phias -H git clone --branch=master https://github.com/jar3b/py-phias.git py-phias
sudo pip install -r requirements.txt
cd /var/www/
sudo mkdir -p fias-api
sudo chown fias: /var/www/fias-api
sudo -H -u fias git clone --branch=py3 https://github.com/jar3b/py-phias.git fias-api
cd fias-api
sudo pip3 install -r requirements.txt
```
2. Иные пути установки ... (soon)
## Настройка
### Первоначальная настройка базы данных
1. Настроим конфиг, для этого необходимо изменить параметры в Вашем wsgi-entrypoint (в моем случае
[passenger_wsgi.py](passenger_wsgi.py)): в строке `from config import *` измените _config_ на имя Вашего
конфигурационного файла (создается рядом с wsgi app), пример конфига находится в файле
[config.example.py](config.example.py).
[wsgi.py](wsgi.py)): в строке `from config import *` измените _config_ на имя Вашего
конфигурационного файла (создается рядом с wsgi app)
2. Создадим базу:
- из архива `sudo -u phias python manage.py -b create -s /tmp/fias_xml.rar`
- из директории `sudo -u phias python manage.py -b create -s /tmp/fias_xml_unpacked`
- онлайн, с сервера ФНС `sudo -u phias python manage.py -b create -s http`
- из архива `sudo -u fias python3 manage.py -b create -s /tmp/fias_xml.rar`
- из директории `sudo -u fias python3 manage.py -b create -s /tmp/fias_xml_unpacked`
- онлайн, с сервера ФНС `sudo -u fias python3 manage.py -b create -s http`
- Также, можно указать конкретную версию ФИАС олько_ при http загрузке, с ключом `--update-version <num>`, где num -
номер версии ФИАС, все доступные версии можно получить, выполнив `manage.py -v`.
@ -152,14 +159,134 @@ _Внимание_! Только Python 2.7+ (на 3+ не тестировал)
указанной в `config.folders.temp`, иначе будет Permission denied при попытке bulk-import.
3. Проиндексируем Sphinx:
- Windows: `python manage.py -c -i C:\sphinx\bin\indexer.exe -o C:\sphinx\sphinx.conf`
- Debian: `sudo python manage.py -c -i indexer -o /usr/local/sphinx/etc/sphinx.conf`
- Debian: `sudo python3 manage.py -c -i indexer -o /usr/local/etc/sphinx.conf`
4. Затем запустим searchd:
- Windows:
- Устанавливаем службу: `C:\Sphinx\bin\searchd --install --config C:\Sphinx\sphinx.conf --servicename sphinxsearch`
- и запускаем: `net start sphinxsearch`
- Debian:
- Запустим : `sudo searchd --config /usr/local/sphinx/etc/sphinx.conf`
- если необходимо, добавьте `searchd --config /usr/local/sphinx/etc/sphinx.conf` в `/etc/rc.local` для автостарта
5. Настроим WSGI server, я использую nginx + passenger. Конфиг для passenger - [passenger_wsgi.py](passenger_wsgi.py),
конфиг для nginx - [py-phias.conf](https://gist.github.com/jar3b/f8f5d351e0ea8ae2ed8e). Вы можете
использовать любое приемлемое сочетание.
- Запустим : `sudo searchd --config /usr/local/etc/sphinx.conf`
- если необходимо, добавьте `searchd --config /usr/local/etc/sphinx.conf` в `/etc/rc.local` для автостарта
5. Для проверки работы выполните `sudo -H -u fias python3 wsgi.py`, по адресу
`http://example.com:8087/find/москва`
Вы должны увидеть результаты запроса.
### Установка Web-сервера (для Debian, на примере nginx + gunicorn, без virtualenv)
- Установим nginx и gunicorn:
```
sudo apt-get install nginx
sudo pip3 install gunicorn
```
- Настройте nginx. Примерно так:
```
cd /etc/nginx/sites-available
sudo wget -O fias-api.conf https://gist.githubusercontent.com/jar3b/f8f5d351e0ea8ae2ed8e/raw/2f1b0d2a6f9ce9db017117993954158ccce049dd/py-phias.conf
sudo nano fias-api.conf
```
, отредактируйте и сохраните файл, затем cоздайте линк
```
sudo cp -l fias-api.conf ../sites-enabled/fias-api.conf
```
- Теперь настроим автозапуск gunicorn. Ниже пример конфига для systemd для запуска как сервис, для этого нужно создать
файл `fias.service` в директории `/etc/systemd/system/`
```
[Unit]
Description=Gunicorn instance to serve fias
After=network.target
[Service]
User=fias
Group=www-data
WorkingDirectory=/var/www/fias-api
ExecStart=/usr/local/bin/gunicorn -k gevent_pywsgi --worker-connections 200 --bind unix:/tmp/fias-api-unicorn.sock -m 007 wsgi:application --log-file /var/log/fias_errors.log
[Install]
WantedBy=multi-user.target
```
- Применим изменения: `sudo systemctl daemon-reload`
- Для запуска сервиса используем `sudo systemctl start fias`, для регистрации в автозапуске `sudo systemctl enable fias`
## Api
- `/normalize/<guid>` - актуализирует AOID или AOGUID, на выходе выдает
```
{"aoid": "1d6185b5-25a6-4fe8-9208-e7ddd281926a"}
```
, где _aoid_ - актуальный AOID.
- `/find/<text>?strong=<0,1>`- полнотекстовый поиск по названию адресного объекта. `<text>` - строка поиска.
Если указан параметр `strong=1`, то в массиве будет один результат, или ошибка. Если же флаг не указан, но будет выдано 10
наиболее релевантных результатов.
На выходе будет массив от 1 до 10 элементов:
```
[
{
"cort": 0,
"text": "обл Псковская, р-н Порховский, д Гречушанка",
"ratio": 1537,
"aoid": "1d6185b5-25a6-4fe8-9208-e7ddd281926a"
},
... (up to 10)
]
```
,где _cort_ - количество несовпавших слов, _text_ - полное название адресного объекта, _ratio_ - рейтинг, _aoid_ -
актуальный AOID.
- `/expand/<aoid>` - "раскрывает" AOID, возвращая массив адресных элементов. `<aoid>` - актуальный или неактуальный
AOID
На выходе будет массив из адресных элементов, упорядоченный по AOLEVEL:
```
[
{
"aoguid": "0c5b2444-70a0-4932-980c-b4dc0d3f02b5",
"shortname": "г",
"aoid": "5c8b06f1-518e-496e-b683-7bf917e0d70b",
"formalname": "Москва",
"aolevel": 1,
"socrname": "Город",
"regioncode": 77
},
{
"aoguid": "10409e98-eb2d-4a52-acdd-7166ca7e0e48",
"shortname": "п",
"aoid": "41451677-aad4-4cb9-ba76-2b0eeb156acb",
"formalname": "Вороновское",
"aolevel": 3,
"socrname": "Поселок",
"regioncode": 77
},
{
"aoguid": "266485f4-a204-4382-93ce-7a47ad934869",
"shortname": "ул",
"aoid": "943c8b81-2491-46ee-aee4-48d0c9fada1a",
"formalname": "Новая",
"aolevel": 7,
"socrname": "Улица",
"regioncode": 77
}
]
```
, все поля из таблицы ADDROBJ.
- `/gettext/<aoid>` - возвращает текст для произвольного _aoid_, тект аналогичен тому, который возвращает `/find/<text>`,
на выходе будет такой массив с одним элементом:
```
[
{
"fullname": "г Москва, п Вороновское, п ЛМС, ул Новая"
}
]
```
- Ошибки. Если при выполнении запроса произошла ошибка, то ответ будет таким, объект с одним полем описания ошибки:
```
{
"error": "'Cannot find sentence.'"
}
```

View File

@ -1,7 +1,5 @@
import os
import sys
reload(sys)
cwd_dir = os.getcwd()
sys.path.append(cwd_dir)
sys.setdefaultencoding("utf-8")

View File

@ -1,4 +1 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from .common import *

View File

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
class BasicConfig:
logging = False
debug_print = False
logfile = ""
def __init__(self):

View File

@ -1,8 +1,5 @@
# -*- coding: utf-8 -*-
from traceback import format_exc
import psycopg2.extras
from traceback import format_exc
from aore.miscutils.exceptions import FiasException

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
class DbSchema:
def __init__(self, name, fieldlist, unique_key, xmltag):
self.tablename = name
@ -29,4 +26,3 @@ db_shemas['AOTRIG'] = \
None)
allowed_tables = ["ADDROBJ", "SOCRBASE"]

View File

@ -1,24 +0,0 @@
# -*- coding: utf-8 -*-
from bottle import Bottle
class BottleCL(object):
def __init__(self):
self._app = Bottle()
self.init_routes()
def init_routes(self):
pass
def add_route(self, route_path, handler):
self._app.route(route_path, callback=handler)
def add_error(self, error_code, handler):
if not self._app.error_handler:
self._app.error_handler = {error_code: handler}
else:
self._app.error_handler[error_code] = handler
def start(self, **kwargs):
self._app.run(**kwargs)

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
class FiasException(Exception):
def __str__(self):
return repr(self.args[0])

View File

@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
import re
import Levenshtein
import re
def violet_ratio(pattern, candidate):
@ -13,7 +11,7 @@ def violet_ratio(pattern, candidate):
for i in range(len(arr_pattern) - 1, -1, -1):
max_j = -1
max_ratio = -1
allowed_nums = range(len(arr_candidate) - 1, -1, -1)
allowed_nums = list(range(len(arr_candidate) - 1, -1, -1))
for j in allowed_nums:
ratio = Levenshtein.ratio(arr_pattern[i], arr_candidate[j])
@ -21,10 +19,10 @@ def violet_ratio(pattern, candidate):
max_ratio = ratio
max_j = j
result.append(max_j*abs(max_ratio))
result.append(max_j * abs(max_ratio))
if max_j > -1:
allowed_nums.remove(max_j)
del allowed_nums[max_j]
del arr_candidate[max_j]
return sum(result) - len(arr_candidate)

View File

@ -1,8 +1,5 @@
# -*- coding: utf-8 -*-
import logging
import os
from bottle import template
from aore.config import Folders, DatabaseConfig, SphinxConfig
@ -22,11 +19,11 @@ class SphinxHelper:
os.makedirs(Folders.temp)
# оздаем 3 папки для Сфинкса
if not os.path.exists(SphinxConfig.var_dir+ '/run'):
if not os.path.exists(SphinxConfig.var_dir + '/run'):
os.makedirs(SphinxConfig.var_dir + '/run')
if not os.path.exists(SphinxConfig.var_dir+ '/log'):
if not os.path.exists(SphinxConfig.var_dir + '/log'):
os.makedirs(SphinxConfig.var_dir + '/log')
if not os.path.exists(SphinxConfig.var_dir+ '/data'):
if not os.path.exists(SphinxConfig.var_dir + '/data'):
os.makedirs(SphinxConfig.var_dir + '/data')
def configure_indexer(self, indexer_binary, config_filename):
@ -55,7 +52,7 @@ class SphinxHelper:
logging.info("All indexes were created.")
# remove temp files
for fname, fpath in self.files.iteritems():
for fname, fpath in self.files.items():
try:
os.remove(fpath)
except:
@ -84,10 +81,10 @@ class SphinxHelper:
def __dbexport_sugg_dict(self):
logging.info("Place suggestion dict to DB %s...", self.files['dict.txt'])
dict_dat_fname = os.path.abspath(Folders.temp + "/suggdict.csv")
fname = os.path.abspath(Folders.temp + "/suggdict.csv")
csv_counter = 0
with open(self.files['dict.txt'], "r") as dict_file, open(dict_dat_fname, "w") as exit_file:
with open(self.files['dict.txt'], "r") as dict_file, open(fname, "w") as exit_file:
line = None
while line != '':
nodes = []
@ -111,9 +108,11 @@ class SphinxHelper:
except:
pass
self.aodp.bulk_csv(AoXmlTableEntry.OperationType.update, "AOTRIG", csv_counter, dict_dat_fname)
self.aodp.bulk_csv(AoXmlTableEntry.OperationType.update, "AOTRIG", csv_counter, fname)
logging.info("Done.")
return fname
def __create_ao_index_config(self):
fname = os.path.abspath(Folders.temp + "/addrobj.conf")
logging.info("Creating config %s", fname)
@ -156,7 +155,7 @@ class SphinxHelper:
sphinx_var_path=SphinxConfig.var_dir)
f = open(out_filename, "w")
for fname, fpath in self.files.iteritems():
for fname, fpath in self.files.items():
if ".conf" in fname:
with open(fpath, "r") as conff:
for line in conff:

View File

@ -1,8 +1,5 @@
# -*- coding: utf-8 -*-
def trigram(inp):
inp = u"__"+inp+u"__"
inp = u"__" + inp + u"__"
output = []
for i in range(0, len(inp) - 2):

View File

@ -1,58 +1,62 @@
# -*- coding: utf-8 -*-
import json
import logging
from bottle import response
from borest import app, Route, Error
from bottle import response, request
from aore.search.fiasfactory import FiasFactory
from miscutils.bottlecl import BottleCL
class App(BottleCL):
class App:
_factory = None
def __init__(self, log_filename):
super(App, self).__init__()
logging.basicConfig(format='%(asctime)s %(message)s', level=logging.DEBUG, filename=log_filename)
self._factory = FiasFactory()
App._factory = FiasFactory()
def get_app(self):
return self._app
def init_routes(self):
self.add_route(r'/expand/<aoid:re:[\w]{8}(-[\w]{4}){3}-[\w]{12}>', self.__expand)
self.add_route(r'/normalize/<aoid:re:[\w]{8}(-[\w]{4}){3}-[\w]{12}>', self.__normalize)
self.add_route(r'/gettext/<aoid:re:[\w]{8}(-[\w]{4}){3}-[\w]{12}>', self.__gettext)
self.add_route(r'/find/<text>', self.__find)
self.add_route(r'/find/<text>/<strong>', self.__find)
self.add_error(404, self.basic_error_handler)
self.add_error(500, self.basic_error_handler)
def __expand(self, aoid):
@Route(r'/expand/<aoid:re:[\w]{8}(-[\w]{4}){3}-[\w]{12}>')
class Expand(object):
def get(self, aoid):
response.content_type = 'application/json'
response.set_header('Access-Control-Allow-Origin', '*')
return json.dumps(self._factory.expand(aoid))
return json.dumps(App._factory.expand(aoid))
def __normalize(self, aoid):
@Route(r'/normalize/<aoid:re:[\w]{8}(-[\w]{4}){3}-[\w]{12}>')
class Normalize(object):
def get(self, aoid):
response.content_type = 'application/json'
response.set_header('Access-Control-Allow-Origin', '*')
return json.dumps(self._factory.normalize(aoid))
return json.dumps(App._factory.normalize(aoid))
def __find(self, text, strong=False):
strong = (strong == "strong")
@Route(r'/aoid2aoguid/<aoguid:re:[\w]{8}(-[\w]{4}){3}-[\w]{12}>')
class Convert(object):
def get(self, aoguid):
response.content_type = 'application/json'
response.set_header('Access-Control-Allow-Origin', '*')
return json.dumps(self._factory.find(text, strong))
return json.dumps(App._factory.convert(aoguid))
def __gettext(self, aoid):
@Route(r'/find/<text>')
class Find(object):
def get(self, text):
strong = 'strong' in request.query and request.query.strong == '1'
response.content_type = 'application/json'
response.set_header('Access-Control-Allow-Origin', '*')
return json.dumps(self._factory.gettext(aoid))
return json.dumps(App._factory.find(text, strong))
@Route(r'/gettext/<aoid:re:[\w]{8}(-[\w]{4}){3}-[\w]{12}>')
class GetText(object):
def get(self, aoid):
response.content_type = 'application/json'
response.set_header('Access-Control-Allow-Origin', '*')
return json.dumps(App._factory.gettext(aoid))
@staticmethod
@Error([404, 500])
def basic_error_handler(error):
response.content_type = 'application/json'
response.set_header('Access-Control-Allow-Origin', '*')

View File

@ -1,16 +1,14 @@
# -*- coding: utf-8 -*-
import logging
import re
import urllib
from uuid import UUID
import psycopg2
import re
import traceback
import urllib.parse
from bottle import template
from uuid import UUID
from aore.config import DatabaseConfig, BasicConfig
from aore.dbutils.dbimpl import DBImpl
from search import SphinxSearch
from .search import SphinxSearch
class FiasFactory:
@ -19,6 +17,7 @@ class FiasFactory:
self.searcher = SphinxSearch(self.db)
self.expand_templ = template('aore/templates/postgre/expand_query.sql', aoid="//aoid")
self.normalize_templ = template('aore/templates/postgre/normalize_query.sql', aoid="//aoid")
self.convert_templ = template('aore/templates/postgre/convert_query.sql', aoid="//aoid")
self.gettext_templ = template('aore/templates/postgre/gettext_query.sql', aoid="//aoid")
# Проверка, что строка является действительым UUID v4
@ -38,10 +37,10 @@ class FiasFactory:
if rule == "boolean":
assert isinstance(param, bool), "Invalid parameter type"
if rule == "uuid":
assert (isinstance(param, str) or isinstance(param, unicode)) and self.__check_uuid(
assert isinstance(param, str) and self.__check_uuid(
param), "Invalid parameter value"
if rule == "text":
assert isinstance(param, str) or isinstance(param, unicode), "Invalid parameter type"
assert isinstance(param, str), "Invalid parameter type"
assert len(param) > 3, "Text too short"
pattern = re.compile(r"[A-za-zА-Яа-я \-,.#№]+")
assert pattern.match(param), "Invalid parameter value"
@ -49,18 +48,19 @@ class FiasFactory:
# text - строка поиска
# strong - строгий поиск (True) или "мягкий" (False) (с допущением ошибок, опечаток)
# Строгий используется при импорте из внешних систем (автоматически), где ошибка критична
def find(self, text, strong=False):
try:
text = urllib.unquote(text).decode('utf8')
text = urllib.parse.unquote(str(text))
self.__check_param(text, "text")
self.__check_param(strong, "boolean")
results = self.searcher.find(text, strong)
except Exception, err:
except Exception as err:
if BasicConfig.logging:
logging.error(traceback.format_exc(err))
return dict(error=err.args[0])
logging.error(traceback.format_exc())
if BasicConfig.debug_print:
traceback.print_exc()
return dict(error=str(err))
return results
@ -73,10 +73,33 @@ class FiasFactory:
rows = self.db.get_rows(sql_query, True)
assert len(rows), "Record with this AOID not found in DB"
except Exception, err:
except Exception as err:
if BasicConfig.logging:
logging.error(traceback.format_exc(err))
return dict(error=err.args[0])
logging.error(traceback.format_exc())
if BasicConfig.debug_print:
traceback.print_exc()
return dict(error=str(err))
if len(rows) == 0:
return []
else:
return rows[0]
# Преобразует AOID в AOGUID
def convert(self, aoid: str):
try:
self.__check_param(aoid, "uuid")
sql_query = self.convert_templ.replace("//aoid", aoid)
rows = self.db.get_rows(sql_query, True)
assert len(rows), "Record with this AOID not found in DB"
except Exception as err:
if BasicConfig.logging:
logging.error(traceback.format_exc())
if BasicConfig.debug_print:
traceback.print_exc()
return dict(error=str(err))
if len(rows) == 0:
return []
@ -94,10 +117,12 @@ class FiasFactory:
sql_query = self.expand_templ.replace("//aoid", normalized_id)
rows = self.db.get_rows(sql_query, True)
except Exception, err:
except Exception as err:
if BasicConfig.logging:
logging.error(traceback.format_exc(err))
return dict(error=err.args[0])
logging.error(traceback.format_exc())
if BasicConfig.debug_print:
traceback.print_exc()
return dict(error=str(err))
return rows
@ -111,9 +136,11 @@ class FiasFactory:
rows = self.db.get_rows(sql_query, True)
assert len(rows), "Record with this AOID not found in DB"
except Exception, err:
except Exception as err:
if BasicConfig.logging:
logging.error(traceback.format_exc(err))
return dict(error=err.args[0])
logging.error(traceback.format_exc())
if BasicConfig.debug_print:
traceback.print_exc()
return dict(error=str(err))
return rows

View File

@ -1,18 +1,16 @@
# -*- coding: utf-8 -*-
import Levenshtein
import logging
import re
import time
import Levenshtein
import sphinxapi
import time
from aore.config import BasicConfig
from aore.config import SphinxConfig
from aore.miscutils.exceptions import FiasException
from aore.miscutils.fysearch import violet_ratio
from aore.miscutils.trigram import trigram
from wordentry import WordEntry
from wordvariation import VariationType
from .wordentry import WordEntry
from .wordvariation import VariationType
class SphinxSearch:
@ -62,7 +60,7 @@ class SphinxSearch:
self.client_show.SetSortMode(sphinxapi.SPH_SORT_EXTENDED, "krank DESC")
def __get_suggest(self, word, rating_limit, count):
word_len = str(len(word) / 2)
word_len = len(word)
trigrammed_word = '"{}"/1'.format(trigram(word))
self.__configure(SphinxConfig.index_sugg, word_len)
@ -95,7 +93,7 @@ class SphinxSearch:
return outlist
# Получает список объектов (слово)
def __get_word_entries(self, words, strong):
def __get_word_entries(self, words):
we_list = []
for word in words:
if word != '':
@ -108,17 +106,16 @@ class SphinxSearch:
# text - текст найденного адресного объекта
# ratio - рейтинг найденного пункта
# cort - рейтинг количества совпавших слов
def find(self, text, strong):
def split_phrase(phrase):
phrase = unicode(phrase).lower()
phrase = phrase.lower()
return re.split(r"[ ,:.#$]+", phrase)
# сплитим текст на слова
words = split_phrase(text)
# получаем список объектов (слов)
word_entries = self.__get_word_entries(words, strong)
word_entries = self.__get_word_entries(words)
word_count = len(word_entries)
# проверяем, есть ли вообще что-либо в списке объектов слов (или же все убрали как частое)
@ -169,23 +166,23 @@ class SphinxSearch:
parsed_ids.append(match['attrs']['aoid'])
results.append(
dict(aoid=match['attrs']['aoid'],
text=unicode(match['attrs']['fullname']),
text=str(match['attrs']['fullname']),
ratio=match['attrs']['krank'],
cort=i))
# При строгом поиске нам надо еще добавить fuzzy и выбрать самое большое значение при отклонении
# выше заданного
# выше заданного, по сути переопределяем ratio
if strong:
for result in results:
result['strong_rank'] = violet_ratio(text, result['text'].lower())
result['ratio'] = violet_ratio(text, result['text'].lower())
# Сортируем по убыванию признака
results.sort(key=lambda x: x['strong_rank'], reverse=True)
results.sort(key=lambda x: x['ratio'], reverse=True)
# Если подряд два одинаково релеватных результата - это плохо, на автомат такое отдавать нельзя
if abs(results[0]['strong_rank'] - results[1]['strong_rank']) == 0.0:
if abs(results[0]['ratio'] - results[1]['ratio']) == 0.0:
raise FiasException("No matches")
else:
return results[0]
results = [results[0]]
return results

View File

@ -1,10 +1,13 @@
# -*- coding: utf-8 -*-
import re
from aore.config import SphinxConfig
from aore.search.wordvariation import WordVariation, VariationType
def cleanup_string(word):
return word.replace('-', '').replace('@', '').replace('#', '')
class WordEntry:
# Варианты распеределния для слов с первыми двумя символами, где:
# 0 - не найдено, 1 - найдено одно, x - найдено много (>1)
@ -51,14 +54,14 @@ class WordEntry:
def __init__(self, db, word):
self.db = db
self.bare_word = str(word)
self.word = self.__cleanify(self.bare_word)
self.word_len = len(unicode(self.word))
self.bare_word = word
self.word = cleanup_string(self.bare_word)
self.word_len = len(self.word)
self.parameters = dict(IS_FREQ=False, SOCR_WORD=None)
self.ranks = self.__init_ranks()
# Заполняем параметры слова
for mt_name, mt_values in self.match_types.iteritems():
for mt_name, mt_values in self.match_types.items():
self.__dict__[mt_name] = False
for mt_value in mt_values:
self.__dict__[mt_name] = self.__dict__[mt_name] or re.search(mt_value, self.ranks)
@ -72,9 +75,6 @@ class WordEntry:
self.MT_LAST_STAR = False
self.MT_AS_IS = True
def __cleanify(self, word):
return word.replace('-', '').replace('@', '')
def variations_generator(self, strong, suggestion_func):
default_var_type = VariationType.normal
# Если слово встречается часто, ставим у всех вариантов тип VariationType.freq
@ -115,8 +115,9 @@ class WordEntry:
"UNION ALL SELECT COUNT(*), NULL FROM \"AOTRIG\" WHERE word='{}' " \
"UNION ALL SELECT COUNT(*), MAX(scname) FROM \"SOCRBASE\" WHERE socrname ILIKE '{}'" \
"UNION ALL SELECT COUNT(*), NULL FROM \"SOCRBASE\" WHERE scname ILIKE '{}'" \
"UNION ALL SELECT frequency, NULL FROM \"AOTRIG\" WHERE word='{}';".format(
self.word, self.word_len, self.word, self.bare_word, self.bare_word, self.word)
"UNION ALL SELECT frequency, NULL FROM \"AOTRIG\" WHERE word='{}';".format(self.word, self.word_len,
self.word, self.bare_word,
self.bare_word, self.word)
result = self.db.get_rows(sql_qry)
@ -146,8 +147,5 @@ class WordEntry:
def get_type(self):
return ", ".join([x for x in self.match_types if self.__dict__[x]])
def __unicode__(self):
return self.word
def __str__(self):
return str(self.word)

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from enum import Enum # Типы вариаций слова

0
aore/static/.gitkeep Normal file
View File

View File

@ -0,0 +1 @@
SELECT AOGUID FROM "ADDROBJ" WHERE AOID=(SELECT AOID FROM "ADDROBJ" WHERE AOID='{{ aoid }}' OR AOGUID='{{ aoid }}') AND ACTSTATUS=True AND LIVESTATUS=True AND NEXTID IS NULL LIMIT 1;

View File

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
import codecs
import os
from aore.config import Folders
@ -38,7 +38,7 @@ class AoDataParser:
# Prepare to next iteration
self.counter = 0
self.currentpage += 1
self.csv_file = open(self.base_filename.format(self.currentpage), "w")
self.csv_file = codecs.open(self.base_filename.format(self.currentpage), "w", "utf-8")
exit_nodes = list()
for allowed_field in self.allowed_fields:

View File

@ -1,15 +1,12 @@
# -*- coding: utf-8 -*-
import logging
import os.path
from traceback import format_exc
import rarfile
import requests
from traceback import format_exc
from aore.config import Folders, UnrarConfig
from aore.miscutils.exceptions import FiasException
from aoxmltableentry import AoXmlTableEntry
from .aoxmltableentry import AoXmlTableEntry
class AoRar:

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import re
from enum import Enum
@ -22,7 +20,7 @@ class AoXmlTableEntry:
@classmethod
def from_dir(cls, file_name, path):
# for extracted into folder
return AoXmlTableEntry(file_name, lambda: open(path + file_name))
return AoXmlTableEntry(file_name, lambda: open(path + file_name, 'rb'))
def __init__(self, file_name, lamda_open):
matchings = re.search('^(AS_)(DEL_)*([A-Z]+)', file_name)
@ -42,8 +40,5 @@ class AoXmlTableEntry:
def close(self):
self.file_descriptor.close()
def __unicode__(self):
return "Entry for {} table {}".format(self.operation_type, self.table_name)
def __str__(self):
return "Entry for {} table {}".format(self.operation_type, self.table_name)

View File

@ -1,7 +1,4 @@
# -*- coding: utf-8 -*-
import logging
import psycopg2
from bottle import template
@ -66,4 +63,3 @@ class DbHandler:
self.db.execute(sql_query)
logging.info("All indexes was deleted.")

View File

@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
from pysimplesoap.client import SoapClient
class SoapReceiver:
def __init__(self):
self.client = SoapClient(
location="http://fias.nalog.ru/WebServices/Public/DownloadService.asmx",
location="https://fias.nalog.ru/WebServices/Public/DownloadService.asmx",
action='http://fias.nalog.ru/WebServices/Public/DownloadService.asmx/',
namespace="http://fias.nalog.ru/WebServices/Public/DownloadService.asmx",
soap_ns='soap', trace=False, ns=False)

View File

@ -1,9 +1,6 @@
# -*- coding: utf-8 -*-
import logging
from os import walk, path
import psycopg2
from os import walk, path
from aore.config import DatabaseConfig
from aore.dbutils.dbimpl import DBImpl
@ -62,9 +59,9 @@ class Updater:
mode = None
while not mode:
try:
mode = int(raw_input('Enter FIAS update version (3 digit):'))
mode = int(input('Enter FIAS update version (3 digit):'))
except ValueError:
print "Not a valid fias version, try again."
print("Not a valid fias version, try again.")
return mode

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from lxml import etree
@ -7,7 +5,8 @@ class XMLParser:
def __init__(self, parse_function):
self.parse_function = parse_function
def fast_iter(self, context, func, *args, **kwargs):
@staticmethod
def fast_iter(context, func, *args, **kwargs):
for event, elem in context:
# print event
func(elem, *args, **kwargs)

26
config.example_lin.py Normal file
View File

@ -0,0 +1,26 @@
from aore import config
# Config section
# Address and port where sphinx was listening,
# may be a unix socket like 'unix:///tmp/fias-api.sock'
# or TCP socket like '127.0.0.1:9312'
config.SphinxConfig.listen = "unix:///tmp/fias-api.sock"
# Base sphinx folder
config.SphinxConfig.var_dir = "/etc/sphinx"
# DB config
config.DatabaseConfig.database = "fias_db"
config.DatabaseConfig.host = "127.0.0.1"
config.DatabaseConfig.port = 5432
config.DatabaseConfig.user = "postgres"
config.DatabaseConfig.password = "postgres"
# Path to unrar, in Linux may be 'unrar'
config.UnrarConfig.path = "unrar"
# Temp folder, in Linux may be '/tmp/myfolder'
config.Folders.temp = "/tmp/fitmp"
config.BasicConfig.logging = True
config.BasicConfig.debug_print = False
config.BasicConfig.logfile = "pyphias.log"

View File

@ -1,17 +1,17 @@
# -*- coding: utf-8 -*-
from aore import config
# Config section
# Address and port where sphinx was listening,
# may be a unix socket like 'unix://tmp/pyphias.sock'
# may be a unix socket like 'unix:///tmp/fias-api.sock'
# or TCP socket like '127.0.0.1:9312'
config.SphinxConfig.listen = "127.0.0.1:9312"
# Base sphinx folder
config.SphinxConfig.var_dir = "C:\\Sphinx"
# DB config
config.DatabaseConfig.database = "fias_db"
config.DatabaseConfig.host = "192.168.0.1"
config.DatabaseConfig.host = "127.0.0.1"
config.DatabaseConfig.port = 5432
config.DatabaseConfig.user = "postgres"
config.DatabaseConfig.password = "postgres"
@ -22,4 +22,5 @@ config.UnrarConfig.path = "C:\\Program Files\\WinRAR\\unrar.exe"
config.Folders.temp = "E:\\!TEMP"
config.BasicConfig.logging = True
config.BasicConfig.debug_print = False
config.BasicConfig.logfile = "pyphias.log"

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
import logging
import optparse
import sys
@ -23,7 +22,7 @@ def print_fias_versions():
all_versions = imp.get_update_list()
print("Installed version: {}".format(current_version))
print("Avaliable updates:")
print("Available updates:")
print("Number\t\tDate")
for upd in all_versions:
mark_current = (' ', '*')[int(upd['intver']) == current_version]
@ -60,6 +59,14 @@ def get_allowed_updates(updates_str, mode="create"):
yield all_versions[-1]
else:
assert len(user_defined_list) == 1, "Ony single update number allowed for DB create"
processed = False
for ver in all_versions:
if ver['intver'] == user_defined_list[0]:
processed = True
yield ver
else:
if not processed:
raise Exception("Update #%d not found on remote server" % user_defined_list[0])
if mode == "update":
for uv in all_versions:
uv_ver = uv['intver']

View File

@ -1,10 +1,10 @@
lxml>=3.4.0
bottle>=0.12.9
psycopg2>=2.6.1
PySimpleSOAP==1.16
lxml>=3.6.0
bottle==0.12.13
psycopg2==2.7.5
PySimpleSOAP==1.16.2
python-Levenshtein==0.12.0
enum34>=1.0.0
rarfile
rarfile==3.0
requests>=2.8.1
soap2py==1.16
sphinxapi
soap2py>=1.16
git+https://github.com/jar3b/sphinx-py3-api
bottle-oop-rest>=0.0.5

View File

@ -8,11 +8,12 @@ try:
except ImportError:
assert "No config"
# Define main app
phias_app = phias.App(config.BasicConfig.logfile)
# Create main app
phias.App(config.BasicConfig.logfile)
# Define wsgi app
application = phias_app.get_app()
application = phias.app
# Run bottle WSGI server if no external
if __name__ == '__main__':
phias_app.start(host='0.0.0.0', port=8087, debug=True)
application.run(host='0.0.0.0', port=8087, debug=True)