Compare commits

..

No commits in common. "master" and "py2" have entirely different histories.
master ... py2

32 changed files with 269 additions and 394 deletions

1
.gitignore vendored
View File

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

View File

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

259
README.md
View File

@ -1,14 +1,11 @@
# py-phias
Python application that can operate with FIAS (Russian Address Object DB)
Простое приложение для работы с БД ФИАС, написано для Python 3, использует БД PostgreSQL
Простое приложение для работы с БД ФИАС, написано для Python 2.7, использует БД PostgreSQL
## Содержание
- [Возможности](#Возможности)
- [Установка](#Установка)
- [Настройка](#Настройка)
- [API](#Api)
## Возможности
1. API (выходной формат - JSON), основные функции которого:
@ -25,14 +22,12 @@ Python application that can operate with FIAS (Russian Address Object DB)
## Установка
Протестирована работа на следующих ОС: [Windows](#windows) (8.1, 10) и [Debian](#debian-linux) Jessie, Stretch.
Необходима версия Python == 3.5
Протестирована работа на следующих ОС: [Windows](#windows) (8.1) и [Debian](#debian-linux) Jessie
### Зависимости
_Внимание_! Только Python 3 (для 2.7 пока есть отдельная ветка), только PostgreSQL, только Sphinx. MySQL/MariaDB,
ElasticSearch/Solr не поддерживаются и, скорее всего, не будут.
_Внимание_! Только Python 2.7+ (на 3+ не тестировал), только PostgreSQL, только Sphinx. MySQL/MariaDB, ElasticSearch/Solr
не поддерживаются и, скорее всего, не будут.
Для работы приложения необходимо достаточное кол-во RAM (1Gb+) и ~5.5Gb места на диске
(3-3.5Gb для скачивания архива с базой, 350-400Mb для индексов Sphinx, 1Gb для базы). Также необходимы root права
@ -43,38 +38,33 @@ ElasticSearch/Solr не поддерживаются и, скорее всего
Предварительно обязательно установить и настроить:
1. Python 3, pip
1. Python 2.7.x, pip
Для Windows качаем - ставим, для Debian:
```
sudo apt-get install python3-setuptools
sudo easy_install3 pip
sudo pip3 install --upgrade pip
sudo apt-get install python-setuptools
sudo easy_install pip
sudo pip install --upgrade pip
```
2. PostgreSql 9.5 и выше (из-за синтаксиса _ON CONFLICT ... DO_)
Для Windows, как обычно, [качаем](http://www.enterprisedb.com/products-services-training/pgdownload#windows) - ставим,
для Debian 8 и ниже:
Для Windows, как обычно, [качаем](http://www.enterprisedb.com/products-services-training/pgdownload#windows) - ставим, для Debian:
```
sudo sh -c 'echo deb http://apt.postgresql.org/pub/repos/apt/ jessie-pgdg main 9.5 > /etc/apt/sources.list.d/postgresql.list'
sudo sh -c 'echo deb http://apt.postgresql.org/pub/repos/apt/ trusty-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 --no-create-home fias
sudo -u postgres psql
postgres=# CREATE DATABASE fias_db;
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 fias psql -d fias_db -U fias
postgres=# CREATE EXTENSION pg_trgm SCHEMA public;
postgres=# \q
sudo adduser phias
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=# \q
sudo -u phias psql -d fias_db -U phias
postgres=# CREATE EXTENSION pg_trgm SCHEMA public;
```
3. Sphinx 2.2.1 и новее:
@ -89,66 +79,69 @@ ElasticSearch/Solr не поддерживаются и, скорее всего
make
sudo make install
```
, не забудте установить _build-essential_ перед этим (касается Debian).
4. Web-сервер с поддержкой WSGI, любой, по Вашему желанию.
### Windows
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:
1. Установить lxml, скачав [отсюда](https://pypi.python.org/pypi/lxml/3.5.0).
2. Установить unrar.exe (можно установить WinRar целиком).
3. Установить sphinxapi последней версии (либо взять из директории Sphinx):
```
pip install https://github.com/jar3b/sphinx-py3-api/zipball/master
python -m pip install https://github.com/Romamo/sphinxapi/zipball/master
```
4. Установить приложение, скачав релиз `https://github.com/jar3b/py-phias/archive/v0.0.2.zip`, распакуйте его в удобное
Вам место и запустите оттуда `python -m pip install -r requirements.txt`
### Debian Linux
1. Установить unrar (non-free):
1. Установить libxml:
```
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 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 apt-get update
sudo apt-get install unrar
```
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. Установим приложение из репозитория:
3. Установить sphinxapi последней версии:
```
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
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
```
2. Иные пути установки ... (soon)
## Настройка
### Первоначальная настройка базы данных
1. Настроим конфиг, для этого необходимо изменить параметры в Вашем wsgi-entrypoint (в моем случае
[wsgi.py](wsgi.py)): в строке `from config import *` измените _config_ на имя Вашего
конфигурационного файла (создается рядом с wsgi app)
[passenger_wsgi.py](passenger_wsgi.py)): в строке `from config import *` измените _config_ на имя Вашего
конфигурационного файла (создается рядом с wsgi app), пример конфига находится в файле
[config.example.py](config.example.py).
2. Создадим базу:
- из архива `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`
- из архива `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`
- Также, можно указать конкретную версию ФИАС олько_ при http загрузке, с ключом `--update-version <num>`, где num -
номер версии ФИАС, все доступные версии можно получить, выполнив `manage.py -v`.
@ -159,134 +152,14 @@ sudo apt-get install python3-dev
указанной в `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 python3 manage.py -c -i indexer -o /usr/local/etc/sphinx.conf`
- Debian: `sudo python manage.py -c -i indexer -o /usr/local/sphinx/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/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.'"
}
```
- Запустим : `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). Вы можете
использовать любое приемлемое сочетание.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,24 @@
# -*- 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,3 +1,6 @@
# -*- coding: utf-8 -*-
class FiasException(Exception):
def __str__(self):
return repr(self.args[0])

View File

@ -1,6 +1,8 @@
import Levenshtein
# -*- coding: utf-8 -*-
import re
import Levenshtein
def violet_ratio(pattern, candidate):
arr_pattern = re.split(r"[ ,:.#$-]+", pattern)
@ -11,7 +13,7 @@ def violet_ratio(pattern, candidate):
for i in range(len(arr_pattern) - 1, -1, -1):
max_j = -1
max_ratio = -1
allowed_nums = list(range(len(arr_candidate) - 1, -1, -1))
allowed_nums = range(len(arr_candidate) - 1, -1, -1)
for j in allowed_nums:
ratio = Levenshtein.ratio(arr_pattern[i], arr_candidate[j])
@ -19,10 +21,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:
del allowed_nums[max_j]
allowed_nums.remove(max_j)
del arr_candidate[max_j]
return sum(result) - len(arr_candidate)

View File

@ -1,5 +1,8 @@
# -*- coding: utf-8 -*-
import logging
import os
from bottle import template
from aore.config import Folders, DatabaseConfig, SphinxConfig
@ -19,11 +22,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):
@ -52,7 +55,7 @@ class SphinxHelper:
logging.info("All indexes were created.")
# remove temp files
for fname, fpath in self.files.items():
for fname, fpath in self.files.iteritems():
try:
os.remove(fpath)
except:
@ -81,10 +84,10 @@ class SphinxHelper:
def __dbexport_sugg_dict(self):
logging.info("Place suggestion dict to DB %s...", self.files['dict.txt'])
fname = os.path.abspath(Folders.temp + "/suggdict.csv")
dict_dat_fname = os.path.abspath(Folders.temp + "/suggdict.csv")
csv_counter = 0
with open(self.files['dict.txt'], "r") as dict_file, open(fname, "w") as exit_file:
with open(self.files['dict.txt'], "r") as dict_file, open(dict_dat_fname, "w") as exit_file:
line = None
while line != '':
nodes = []
@ -108,11 +111,9 @@ class SphinxHelper:
except:
pass
self.aodp.bulk_csv(AoXmlTableEntry.OperationType.update, "AOTRIG", csv_counter, fname)
self.aodp.bulk_csv(AoXmlTableEntry.OperationType.update, "AOTRIG", csv_counter, dict_dat_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)
@ -155,7 +156,7 @@ class SphinxHelper:
sphinx_var_path=SphinxConfig.var_dir)
f = open(out_filename, "w")
for fname, fpath in self.files.items():
for fname, fpath in self.files.iteritems():
if ".conf" in fname:
with open(fpath, "r") as conff:
for line in conff:

View File

@ -1,5 +1,8 @@
# -*- 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,62 +1,58 @@
# -*- coding: utf-8 -*-
import json
import logging
from borest import app, Route, Error
from bottle import response, request
from bottle import response
from aore.search.fiasfactory import FiasFactory
from miscutils.bottlecl import BottleCL
class App:
_factory = None
class App(BottleCL):
def __init__(self, log_filename):
super(App, self).__init__()
logging.basicConfig(format='%(asctime)s %(message)s', level=logging.DEBUG, filename=log_filename)
App._factory = FiasFactory()
self._factory = FiasFactory()
@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', '*')
def get_app(self):
return self._app
return json.dumps(App._factory.expand(aoid))
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)
@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', '*')
def __expand(self, aoid):
response.content_type = 'application/json'
response.set_header('Access-Control-Allow-Origin', '*')
return json.dumps(App._factory.normalize(aoid))
return json.dumps(self._factory.expand(aoid))
@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', '*')
def __normalize(self, aoid):
response.content_type = 'application/json'
response.set_header('Access-Control-Allow-Origin', '*')
return json.dumps(App._factory.convert(aoguid))
return json.dumps(self._factory.normalize(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', '*')
def __find(self, text, strong=False):
strong = (strong == "strong")
response.content_type = 'application/json'
response.set_header('Access-Control-Allow-Origin', '*')
return json.dumps(App._factory.find(text, strong))
return json.dumps(self._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', '*')
def __gettext(self, aoid):
response.content_type = 'application/json'
response.set_header('Access-Control-Allow-Origin', '*')
return json.dumps(App._factory.gettext(aoid))
return json.dumps(self._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,14 +1,16 @@
# -*- coding: utf-8 -*-
import logging
import psycopg2
import re
import traceback
import urllib.parse
from bottle import template
import urllib
from uuid import UUID
import psycopg2
import traceback
from bottle import template
from aore.config import DatabaseConfig, BasicConfig
from aore.dbutils.dbimpl import DBImpl
from .search import SphinxSearch
from search import SphinxSearch
class FiasFactory:
@ -17,7 +19,6 @@ 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
@ -37,30 +38,29 @@ class FiasFactory:
if rule == "boolean":
assert isinstance(param, bool), "Invalid parameter type"
if rule == "uuid":
assert isinstance(param, str) and self.__check_uuid(
assert (isinstance(param, str) or isinstance(param, unicode)) and self.__check_uuid(
param), "Invalid parameter value"
if rule == "text":
assert isinstance(param, str), "Invalid parameter type"
assert isinstance(param, str) or isinstance(param, unicode), "Invalid parameter type"
assert len(param) > 3, "Text too short"
pattern = re.compile(r"[A-za-zА-Яа-я \-,.#№]+")
assert pattern.match(param), "Invalid parameter value"
# text - строка поиска
# strong - строгий поиск (True) или "мягкий" (False) (с допущением ошибок, опечаток)
# Строгий используется при импорте из внешних систем (автоматически), где ошибка критична
# text - строка поиска
# strong - строгий поиск (True) или "мягкий" (False) (с допущением ошибок, опечаток)
# Строгий используется при импорте из внешних систем (автоматически), где ошибка критична
def find(self, text, strong=False):
try:
text = urllib.parse.unquote(str(text))
text = urllib.unquote(text).decode('utf8')
self.__check_param(text, "text")
self.__check_param(strong, "boolean")
results = self.searcher.find(text, strong)
except Exception as err:
except Exception, err:
if BasicConfig.logging:
logging.error(traceback.format_exc())
if BasicConfig.debug_print:
traceback.print_exc()
return dict(error=str(err))
logging.error(traceback.format_exc(err))
return dict(error=err.args[0])
return results
@ -73,33 +73,10 @@ class FiasFactory:
rows = self.db.get_rows(sql_query, True)
assert len(rows), "Record with this AOID not found in DB"
except Exception as err:
except Exception, 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 []
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))
logging.error(traceback.format_exc(err))
return dict(error=err.args[0])
if len(rows) == 0:
return []
@ -117,12 +94,10 @@ class FiasFactory:
sql_query = self.expand_templ.replace("//aoid", normalized_id)
rows = self.db.get_rows(sql_query, True)
except Exception as err:
except Exception, err:
if BasicConfig.logging:
logging.error(traceback.format_exc())
if BasicConfig.debug_print:
traceback.print_exc()
return dict(error=str(err))
logging.error(traceback.format_exc(err))
return dict(error=err.args[0])
return rows
@ -136,11 +111,9 @@ class FiasFactory:
rows = self.db.get_rows(sql_query, True)
assert len(rows), "Record with this AOID not found in DB"
except Exception as err:
except Exception, err:
if BasicConfig.logging:
logging.error(traceback.format_exc())
if BasicConfig.debug_print:
traceback.print_exc()
return dict(error=str(err))
logging.error(traceback.format_exc(err))
return dict(error=err.args[0])
return rows

View File

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

View File

@ -1,13 +1,10 @@
# -*- 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)
@ -54,14 +51,14 @@ class WordEntry:
def __init__(self, db, word):
self.db = db
self.bare_word = word
self.word = cleanup_string(self.bare_word)
self.word_len = len(self.word)
self.bare_word = str(word)
self.word = self.__cleanify(self.bare_word)
self.word_len = len(unicode(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.items():
for mt_name, mt_values in self.match_types.iteritems():
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)
@ -75,6 +72,9 @@ 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,9 +115,8 @@ 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)
@ -147,5 +146,8 @@ 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,3 +1,4 @@
# -*- coding: utf-8 -*-
from enum import Enum # Типы вариаций слова

View File

View File

@ -1 +0,0 @@
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 @@
import codecs
# -*- coding: utf-8 -*-
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 = codecs.open(self.base_filename.format(self.currentpage), "w", "utf-8")
self.csv_file = open(self.base_filename.format(self.currentpage), "w")
exit_nodes = list()
for allowed_field in self.allowed_fields:

View File

@ -1,12 +1,15 @@
# -*- 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,3 +1,5 @@
# -*- coding: utf-8 -*-
import re
from enum import Enum
@ -20,7 +22,7 @@ class AoXmlTableEntry:
@classmethod
def from_dir(cls, file_name, path):
# for extracted into folder
return AoXmlTableEntry(file_name, lambda: open(path + file_name, 'rb'))
return AoXmlTableEntry(file_name, lambda: open(path + file_name))
def __init__(self, file_name, lamda_open):
matchings = re.search('^(AS_)(DEL_)*([A-Z]+)', file_name)
@ -40,5 +42,8 @@ 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,4 +1,7 @@
# -*- coding: utf-8 -*-
import logging
import psycopg2
from bottle import template
@ -63,3 +66,4 @@ class DbHandler:
self.db.execute(sql_query)
logging.info("All indexes was deleted.")

View File

@ -1,10 +1,11 @@
from pysimplesoap.client import SoapClient
# -*- coding: utf-8 -*-
from pysimplesoap.client import SoapClient
class SoapReceiver:
def __init__(self):
self.client = SoapClient(
location="https://fias.nalog.ru/WebServices/Public/DownloadService.asmx",
location="http://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,7 +1,10 @@
# -*- coding: utf-8 -*-
import logging
import psycopg2
from os import walk, path
import psycopg2
from aore.config import DatabaseConfig
from aore.dbutils.dbimpl import DBImpl
from aore.dbutils.dbschemas import allowed_tables, db_shemas
@ -59,9 +62,9 @@ class Updater:
mode = None
while not mode:
try:
mode = int(input('Enter FIAS update version (3 digit):'))
mode = int(raw_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,3 +1,5 @@
# -*- coding: utf-8 -*-
from lxml import etree
@ -5,8 +7,7 @@ class XMLParser:
def __init__(self, parse_function):
self.parse_function = parse_function
@staticmethod
def fast_iter(context, func, *args, **kwargs):
def fast_iter(self, context, func, *args, **kwargs):
for event, elem in context:
# print event
func(elem, *args, **kwargs)

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/fias-api.sock'
# or TCP socket like '127.0.0.1:9312'
# may be a unix socket like 'unix://tmp/pyphias.sock'
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 = "127.0.0.1"
config.DatabaseConfig.host = "192.168.0.1"
config.DatabaseConfig.port = 5432
config.DatabaseConfig.user = "postgres"
config.DatabaseConfig.password = "postgres"
@ -22,5 +22,4 @@ 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,26 +0,0 @@
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,3 +1,4 @@
# -*- coding: utf-8 -*-
import logging
import optparse
import sys
@ -22,7 +23,7 @@ def print_fias_versions():
all_versions = imp.get_update_list()
print("Installed version: {}".format(current_version))
print("Available updates:")
print("Avaliable updates:")
print("Number\t\tDate")
for upd in all_versions:
mark_current = (' ', '*')[int(upd['intver']) == current_version]
@ -59,14 +60,6 @@ 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

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

View File

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