Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3b47bfd3a3 | ||
|
966bec093f | ||
|
d03aa0a78c | ||
|
67cfd34a66 | ||
|
03d432cb25 | ||
|
63815b0871 | ||
|
9d4928a9e5 | ||
|
ec6fc6a8c0 | ||
|
1452debf94 | ||
|
56ba93b1b7 | ||
|
85e1ecfbaa | ||
|
f2659b0010 | ||
|
e11a9811c9 | ||
|
f688e0c411 | ||
|
79d14ce13e | ||
|
afd61dae19 | ||
|
2f5df29d0a | ||
|
cf74075b00 | ||
|
f079a1edbb | ||
|
eeebf5f8d3 | ||
|
d4668833dc | ||
|
7a2ba33d49 | ||
|
67f64b7aec | ||
|
893d1576e3 | ||
|
552a6355c7 | ||
|
3e6ccc96a1 | ||
|
fc3d2f5e64 | ||
|
27efa759ef | ||
|
72d0036879 | ||
|
682200c4c1 | ||
|
c7f0de74a5 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
|
.idea/
|
||||||
*.log
|
*.log
|
||||||
config.py
|
config.py
|
@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2016, hellotan
|
Copyright (c) 2020, jar3b
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
259
README.md
259
README.md
@ -1,11 +1,14 @@
|
|||||||
# py-phias
|
# py-phias
|
||||||
|
|
||||||
Python application that can operate with FIAS (Russian Address Object DB)
|
Python application that can operate with FIAS (Russian Address Object DB)
|
||||||
|
|
||||||
Простое приложение для работы с БД ФИАС, написано для Python 2.7, использует БД PostgreSQL
|
Простое приложение для работы с БД ФИАС, написано для Python 3, использует БД PostgreSQL
|
||||||
|
|
||||||
## Содержание
|
## Содержание
|
||||||
- [Возможности](#Возможности)
|
- [Возможности](#Возможности)
|
||||||
- [Установка](#Установка)
|
- [Установка](#Установка)
|
||||||
- [Настройка](#Настройка)
|
- [Настройка](#Настройка)
|
||||||
|
- [API](#Api)
|
||||||
|
|
||||||
## Возможности
|
## Возможности
|
||||||
1. API (выходной формат - JSON), основные функции которого:
|
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 места на диске
|
Для работы приложения необходимо достаточное кол-во RAM (1Gb+) и ~5.5Gb места на диске
|
||||||
(3-3.5Gb для скачивания архива с базой, 350-400Mb для индексов Sphinx, 1Gb для базы). Также необходимы root права
|
(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:
|
Для Windows качаем - ставим, для Debian:
|
||||||
```
|
```
|
||||||
sudo apt-get install python-setuptools
|
sudo apt-get install python3-setuptools
|
||||||
sudo easy_install pip
|
sudo easy_install3 pip
|
||||||
sudo pip install --upgrade pip
|
sudo pip3 install --upgrade pip
|
||||||
```
|
```
|
||||||
|
|
||||||
2. PostgreSql 9.5 и выше (из-за синтаксиса _ON CONFLICT ... DO_)
|
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 -
|
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install postgresql-9.5
|
sudo apt-get install postgresql-9.5
|
||||||
```
|
```
|
||||||
|
, для Debian 9:
|
||||||
|
`sudo apt-get install postgresql`
|
||||||
|
|
||||||
Затем создайте пользователя и базу данных и установите расширение pg_trgm:
|
Затем создайте пользователя и базу данных и установите расширение pg_trgm:
|
||||||
```
|
```
|
||||||
sudo adduser phias
|
sudo adduser --no-create-home fias
|
||||||
sudo -u postgres psql
|
sudo -u postgres psql
|
||||||
postgres=# CREATE DATABASE fias_db;
|
postgres=# CREATE DATABASE fias_db;
|
||||||
postgres=# CREATE USER phias WITH password 'phias';
|
postgres=# CREATE USER fias WITH password 'fias';
|
||||||
postgres=# GRANT ALL privileges ON DATABASE fias_db TO phias;
|
postgres=# GRANT ALL privileges ON DATABASE fias_db TO fias;
|
||||||
postgres=# ALTER USER phias WITH SUPERUSER;
|
postgres=# ALTER USER fias WITH SUPERUSER;
|
||||||
postgres=# \q
|
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=# CREATE EXTENSION pg_trgm SCHEMA public;
|
||||||
|
postgres=# \q
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Sphinx 2.2.1 и новее:
|
3. Sphinx 2.2.1 и новее:
|
||||||
@ -79,69 +89,66 @@ _Внимание_! Только Python 2.7+ (на 3+ не тестировал)
|
|||||||
make
|
make
|
||||||
sudo make install
|
sudo make install
|
||||||
```
|
```
|
||||||
|
, не забудте установить _build-essential_ перед этим (касается Debian).
|
||||||
|
|
||||||
4. Web-сервер с поддержкой WSGI, любой, по Вашему желанию.
|
4. Web-сервер с поддержкой WSGI, любой, по Вашему желанию.
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
1. Установить lxml, скачав [отсюда](https://pypi.python.org/pypi/lxml/3.5.0).
|
1. Установить lxml, скачав whl [отсюда](http://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml) и сделав
|
||||||
2. Установить unrar.exe (можно установить WinRar целиком).
|
`pip install yourmodulename.whl`.
|
||||||
3. Установить sphinxapi последней версии (либо взять из директории Sphinx):
|
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
|
### Debian Linux
|
||||||
1. Установить libxml:
|
1. Установить unrar (non-free):
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo apt-get install python-dev libxml2 libxml2-dev libxslt-dev
|
sudo sh -c 'echo deb ftp://ftp.us.debian.org/debian/ <deb_name> main non-free > /etc/apt/sources.list.d/non-free.list'
|
||||||
```
|
|
||||||
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 update
|
||||||
sudo apt-get install unrar
|
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
|
cd /var/www/
|
||||||
```
|
sudo mkdir -p fias-api
|
||||||
4. Установить, собственно, приложение:
|
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 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 (в моем случае
|
1. Настроим конфиг, для этого необходимо изменить параметры в Вашем wsgi-entrypoint (в моем случае
|
||||||
[passenger_wsgi.py](passenger_wsgi.py)): в строке `from config import *` измените _config_ на имя Вашего
|
[wsgi.py](wsgi.py)): в строке `from config import *` измените _config_ на имя Вашего
|
||||||
конфигурационного файла (создается рядом с wsgi app), пример конфига находится в файле
|
конфигурационного файла (создается рядом с wsgi app)
|
||||||
[config.example.py](config.example.py).
|
|
||||||
2. Создадим базу:
|
2. Создадим базу:
|
||||||
- из архива `sudo -u phias python manage.py -b create -s /tmp/fias_xml.rar`
|
- из архива `sudo -u fias python3 manage.py -b create -s /tmp/fias_xml.rar`
|
||||||
- из директории `sudo -u phias python manage.py -b create -s /tmp/fias_xml_unpacked`
|
- из директории `sudo -u fias python3 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 http`
|
||||||
- Также, можно указать конкретную версию ФИАС _только_ при http загрузке, с ключом `--update-version <num>`, где num -
|
- Также, можно указать конкретную версию ФИАС _только_ при http загрузке, с ключом `--update-version <num>`, где num -
|
||||||
номер версии ФИАС, все доступные версии можно получить, выполнив `manage.py -v`.
|
номер версии ФИАС, все доступные версии можно получить, выполнив `manage.py -v`.
|
||||||
|
|
||||||
@ -152,14 +159,134 @@ _Внимание_! Только Python 2.7+ (на 3+ не тестировал)
|
|||||||
указанной в `config.folders.temp`, иначе будет Permission denied при попытке bulk-import.
|
указанной в `config.folders.temp`, иначе будет Permission denied при попытке bulk-import.
|
||||||
3. Проиндексируем Sphinx:
|
3. Проиндексируем Sphinx:
|
||||||
- Windows: `python manage.py -c -i C:\sphinx\bin\indexer.exe -o C:\sphinx\sphinx.conf`
|
- 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:
|
4. Затем запустим searchd:
|
||||||
- Windows:
|
- Windows:
|
||||||
- Устанавливаем службу: `C:\Sphinx\bin\searchd --install --config C:\Sphinx\sphinx.conf --servicename sphinxsearch`
|
- Устанавливаем службу: `C:\Sphinx\bin\searchd --install --config C:\Sphinx\sphinx.conf --servicename sphinxsearch`
|
||||||
- и запускаем: `net start sphinxsearch`
|
- и запускаем: `net start sphinxsearch`
|
||||||
- Debian:
|
- Debian:
|
||||||
- Запустим : `sudo searchd --config /usr/local/sphinx/etc/sphinx.conf`
|
- Запустим : `sudo searchd --config /usr/local/etc/sphinx.conf`
|
||||||
- если необходимо, добавьте `searchd --config /usr/local/sphinx/etc/sphinx.conf` в `/etc/rc.local` для автостарта
|
- если необходимо, добавьте `searchd --config /usr/local/etc/sphinx.conf` в `/etc/rc.local` для автостарта
|
||||||
5. Настроим WSGI server, я использую nginx + passenger. Конфиг для passenger - [passenger_wsgi.py](passenger_wsgi.py),
|
5. Для проверки работы выполните `sudo -H -u fias python3 wsgi.py`, по адресу
|
||||||
конфиг для nginx - [py-phias.conf](https://gist.github.com/jar3b/f8f5d351e0ea8ae2ed8e). Вы можете
|
`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.'"
|
||||||
|
}
|
||||||
|
```
|
@ -1,7 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
reload(sys)
|
|
||||||
cwd_dir = os.getcwd()
|
cwd_dir = os.getcwd()
|
||||||
sys.path.append(cwd_dir)
|
sys.path.append(cwd_dir)
|
||||||
sys.setdefaultencoding("utf-8")
|
|
||||||
|
@ -1,4 +1 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import absolute_import
|
|
||||||
from .common import *
|
from .common import *
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
|
|
||||||
class BasicConfig:
|
class BasicConfig:
|
||||||
logging = False
|
logging = False
|
||||||
|
debug_print = False
|
||||||
logfile = ""
|
logfile = ""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from traceback import format_exc
|
|
||||||
|
|
||||||
import psycopg2.extras
|
import psycopg2.extras
|
||||||
|
from traceback import format_exc
|
||||||
|
|
||||||
from aore.miscutils.exceptions import FiasException
|
from aore.miscutils.exceptions import FiasException
|
||||||
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
|
|
||||||
class DbSchema:
|
class DbSchema:
|
||||||
def __init__(self, name, fieldlist, unique_key, xmltag):
|
def __init__(self, name, fieldlist, unique_key, xmltag):
|
||||||
self.tablename = name
|
self.tablename = name
|
||||||
@ -29,4 +26,3 @@ db_shemas['AOTRIG'] = \
|
|||||||
None)
|
None)
|
||||||
|
|
||||||
allowed_tables = ["ADDROBJ", "SOCRBASE"]
|
allowed_tables = ["ADDROBJ", "SOCRBASE"]
|
||||||
|
|
||||||
|
@ -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)
|
|
@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
|
|
||||||
class FiasException(Exception):
|
class FiasException(Exception):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return repr(self.args[0])
|
return repr(self.args[0])
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
|
||||||
|
|
||||||
import Levenshtein
|
import Levenshtein
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
def violet_ratio(pattern, candidate):
|
def violet_ratio(pattern, candidate):
|
||||||
@ -13,7 +11,7 @@ def violet_ratio(pattern, candidate):
|
|||||||
for i in range(len(arr_pattern) - 1, -1, -1):
|
for i in range(len(arr_pattern) - 1, -1, -1):
|
||||||
max_j = -1
|
max_j = -1
|
||||||
max_ratio = -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:
|
for j in allowed_nums:
|
||||||
ratio = Levenshtein.ratio(arr_pattern[i], arr_candidate[j])
|
ratio = Levenshtein.ratio(arr_pattern[i], arr_candidate[j])
|
||||||
@ -21,10 +19,10 @@ def violet_ratio(pattern, candidate):
|
|||||||
max_ratio = ratio
|
max_ratio = ratio
|
||||||
max_j = j
|
max_j = j
|
||||||
|
|
||||||
result.append(max_j*abs(max_ratio))
|
result.append(max_j * abs(max_ratio))
|
||||||
|
|
||||||
if max_j > -1:
|
if max_j > -1:
|
||||||
allowed_nums.remove(max_j)
|
del allowed_nums[max_j]
|
||||||
del arr_candidate[max_j]
|
del arr_candidate[max_j]
|
||||||
|
|
||||||
return sum(result) - len(arr_candidate)
|
return sum(result) - len(arr_candidate)
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from bottle import template
|
from bottle import template
|
||||||
|
|
||||||
from aore.config import Folders, DatabaseConfig, SphinxConfig
|
from aore.config import Folders, DatabaseConfig, SphinxConfig
|
||||||
@ -22,11 +19,11 @@ class SphinxHelper:
|
|||||||
os.makedirs(Folders.temp)
|
os.makedirs(Folders.temp)
|
||||||
|
|
||||||
# оздаем 3 папки для Сфинкса
|
# оздаем 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')
|
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')
|
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')
|
os.makedirs(SphinxConfig.var_dir + '/data')
|
||||||
|
|
||||||
def configure_indexer(self, indexer_binary, config_filename):
|
def configure_indexer(self, indexer_binary, config_filename):
|
||||||
@ -55,7 +52,7 @@ class SphinxHelper:
|
|||||||
logging.info("All indexes were created.")
|
logging.info("All indexes were created.")
|
||||||
|
|
||||||
# remove temp files
|
# remove temp files
|
||||||
for fname, fpath in self.files.iteritems():
|
for fname, fpath in self.files.items():
|
||||||
try:
|
try:
|
||||||
os.remove(fpath)
|
os.remove(fpath)
|
||||||
except:
|
except:
|
||||||
@ -84,10 +81,10 @@ class SphinxHelper:
|
|||||||
|
|
||||||
def __dbexport_sugg_dict(self):
|
def __dbexport_sugg_dict(self):
|
||||||
logging.info("Place suggestion dict to DB %s...", self.files['dict.txt'])
|
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
|
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
|
line = None
|
||||||
while line != '':
|
while line != '':
|
||||||
nodes = []
|
nodes = []
|
||||||
@ -111,9 +108,11 @@ class SphinxHelper:
|
|||||||
except:
|
except:
|
||||||
pass
|
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.")
|
logging.info("Done.")
|
||||||
|
|
||||||
|
return fname
|
||||||
|
|
||||||
def __create_ao_index_config(self):
|
def __create_ao_index_config(self):
|
||||||
fname = os.path.abspath(Folders.temp + "/addrobj.conf")
|
fname = os.path.abspath(Folders.temp + "/addrobj.conf")
|
||||||
logging.info("Creating config %s", fname)
|
logging.info("Creating config %s", fname)
|
||||||
@ -156,7 +155,7 @@ class SphinxHelper:
|
|||||||
sphinx_var_path=SphinxConfig.var_dir)
|
sphinx_var_path=SphinxConfig.var_dir)
|
||||||
|
|
||||||
f = open(out_filename, "w")
|
f = open(out_filename, "w")
|
||||||
for fname, fpath in self.files.iteritems():
|
for fname, fpath in self.files.items():
|
||||||
if ".conf" in fname:
|
if ".conf" in fname:
|
||||||
with open(fpath, "r") as conff:
|
with open(fpath, "r") as conff:
|
||||||
for line in conff:
|
for line in conff:
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
|
|
||||||
def trigram(inp):
|
def trigram(inp):
|
||||||
inp = u"__"+inp+u"__"
|
inp = u"__" + inp + u"__"
|
||||||
output = []
|
output = []
|
||||||
|
|
||||||
for i in range(0, len(inp) - 2):
|
for i in range(0, len(inp) - 2):
|
||||||
|
@ -1,58 +1,62 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from borest import app, Route, Error
|
||||||
from bottle import response
|
from bottle import response, request
|
||||||
|
|
||||||
from aore.search.fiasfactory import FiasFactory
|
from aore.search.fiasfactory import FiasFactory
|
||||||
from miscutils.bottlecl import BottleCL
|
|
||||||
|
|
||||||
|
|
||||||
class App(BottleCL):
|
class App:
|
||||||
|
_factory = None
|
||||||
|
|
||||||
def __init__(self, log_filename):
|
def __init__(self, log_filename):
|
||||||
super(App, self).__init__()
|
|
||||||
logging.basicConfig(format='%(asctime)s %(message)s', level=logging.DEBUG, filename=log_filename)
|
logging.basicConfig(format='%(asctime)s %(message)s', level=logging.DEBUG, filename=log_filename)
|
||||||
|
|
||||||
self._factory = FiasFactory()
|
App._factory = FiasFactory()
|
||||||
|
|
||||||
def get_app(self):
|
@Route(r'/expand/<aoid:re:[\w]{8}(-[\w]{4}){3}-[\w]{12}>')
|
||||||
return self._app
|
class Expand(object):
|
||||||
|
def get(self, aoid):
|
||||||
|
response.content_type = 'application/json'
|
||||||
|
response.set_header('Access-Control-Allow-Origin', '*')
|
||||||
|
|
||||||
def init_routes(self):
|
return json.dumps(App._factory.expand(aoid))
|
||||||
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'/normalize/<aoid:re:[\w]{8}(-[\w]{4}){3}-[\w]{12}>')
|
||||||
response.content_type = 'application/json'
|
class Normalize(object):
|
||||||
response.set_header('Access-Control-Allow-Origin', '*')
|
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.normalize(aoid))
|
||||||
|
|
||||||
def __normalize(self, aoid):
|
@Route(r'/aoid2aoguid/<aoguid:re:[\w]{8}(-[\w]{4}){3}-[\w]{12}>')
|
||||||
response.content_type = 'application/json'
|
class Convert(object):
|
||||||
response.set_header('Access-Control-Allow-Origin', '*')
|
def get(self, aoguid):
|
||||||
|
response.content_type = 'application/json'
|
||||||
|
response.set_header('Access-Control-Allow-Origin', '*')
|
||||||
|
|
||||||
return json.dumps(self._factory.normalize(aoid))
|
return json.dumps(App._factory.convert(aoguid))
|
||||||
|
|
||||||
def __find(self, text, strong=False):
|
@Route(r'/find/<text>')
|
||||||
strong = (strong == "strong")
|
class Find(object):
|
||||||
response.content_type = 'application/json'
|
def get(self, text):
|
||||||
response.set_header('Access-Control-Allow-Origin', '*')
|
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.find(text, strong))
|
return json.dumps(App._factory.find(text, strong))
|
||||||
|
|
||||||
def __gettext(self, aoid):
|
@Route(r'/gettext/<aoid:re:[\w]{8}(-[\w]{4}){3}-[\w]{12}>')
|
||||||
response.content_type = 'application/json'
|
class GetText(object):
|
||||||
response.set_header('Access-Control-Allow-Origin', '*')
|
def get(self, aoid):
|
||||||
|
response.content_type = 'application/json'
|
||||||
|
response.set_header('Access-Control-Allow-Origin', '*')
|
||||||
|
|
||||||
return json.dumps(self._factory.gettext(aoid))
|
return json.dumps(App._factory.gettext(aoid))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@Error([404, 500])
|
||||||
def basic_error_handler(error):
|
def basic_error_handler(error):
|
||||||
response.content_type = 'application/json'
|
response.content_type = 'application/json'
|
||||||
response.set_header('Access-Control-Allow-Origin', '*')
|
response.set_header('Access-Control-Allow-Origin', '*')
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
import urllib
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
|
import urllib.parse
|
||||||
from bottle import template
|
from bottle import template
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from aore.config import DatabaseConfig, BasicConfig
|
from aore.config import DatabaseConfig, BasicConfig
|
||||||
from aore.dbutils.dbimpl import DBImpl
|
from aore.dbutils.dbimpl import DBImpl
|
||||||
from search import SphinxSearch
|
from .search import SphinxSearch
|
||||||
|
|
||||||
|
|
||||||
class FiasFactory:
|
class FiasFactory:
|
||||||
@ -19,6 +17,7 @@ class FiasFactory:
|
|||||||
self.searcher = SphinxSearch(self.db)
|
self.searcher = SphinxSearch(self.db)
|
||||||
self.expand_templ = template('aore/templates/postgre/expand_query.sql', aoid="//aoid")
|
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.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")
|
self.gettext_templ = template('aore/templates/postgre/gettext_query.sql', aoid="//aoid")
|
||||||
|
|
||||||
# Проверка, что строка является действительым UUID v4
|
# Проверка, что строка является действительым UUID v4
|
||||||
@ -38,29 +37,30 @@ class FiasFactory:
|
|||||||
if rule == "boolean":
|
if rule == "boolean":
|
||||||
assert isinstance(param, bool), "Invalid parameter type"
|
assert isinstance(param, bool), "Invalid parameter type"
|
||||||
if rule == "uuid":
|
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"
|
param), "Invalid parameter value"
|
||||||
if rule == "text":
|
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"
|
assert len(param) > 3, "Text too short"
|
||||||
pattern = re.compile(r"[A-za-zА-Яа-я \-,.#№]+")
|
pattern = re.compile(r"[A-za-zА-Яа-я \-,.#№]+")
|
||||||
assert pattern.match(param), "Invalid parameter value"
|
assert pattern.match(param), "Invalid parameter value"
|
||||||
|
|
||||||
# text - строка поиска
|
# text - строка поиска
|
||||||
# strong - строгий поиск (True) или "мягкий" (False) (с допущением ошибок, опечаток)
|
# strong - строгий поиск (True) или "мягкий" (False) (с допущением ошибок, опечаток)
|
||||||
# Строгий используется при импорте из внешних систем (автоматически), где ошибка критична
|
# Строгий используется при импорте из внешних систем (автоматически), где ошибка критична
|
||||||
|
|
||||||
def find(self, text, strong=False):
|
def find(self, text, strong=False):
|
||||||
try:
|
try:
|
||||||
text = urllib.unquote(text).decode('utf8')
|
text = urllib.parse.unquote(str(text))
|
||||||
self.__check_param(text, "text")
|
self.__check_param(text, "text")
|
||||||
self.__check_param(strong, "boolean")
|
self.__check_param(strong, "boolean")
|
||||||
|
|
||||||
results = self.searcher.find(text, strong)
|
results = self.searcher.find(text, strong)
|
||||||
except Exception, err:
|
except Exception as err:
|
||||||
if BasicConfig.logging:
|
if BasicConfig.logging:
|
||||||
logging.error(traceback.format_exc(err))
|
logging.error(traceback.format_exc())
|
||||||
return dict(error=err.args[0])
|
if BasicConfig.debug_print:
|
||||||
|
traceback.print_exc()
|
||||||
|
return dict(error=str(err))
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@ -73,10 +73,33 @@ class FiasFactory:
|
|||||||
rows = self.db.get_rows(sql_query, True)
|
rows = self.db.get_rows(sql_query, True)
|
||||||
|
|
||||||
assert len(rows), "Record with this AOID not found in DB"
|
assert len(rows), "Record with this AOID not found in DB"
|
||||||
except Exception, err:
|
except Exception as err:
|
||||||
if BasicConfig.logging:
|
if BasicConfig.logging:
|
||||||
logging.error(traceback.format_exc(err))
|
logging.error(traceback.format_exc())
|
||||||
return dict(error=err.args[0])
|
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:
|
if len(rows) == 0:
|
||||||
return []
|
return []
|
||||||
@ -94,10 +117,12 @@ class FiasFactory:
|
|||||||
|
|
||||||
sql_query = self.expand_templ.replace("//aoid", normalized_id)
|
sql_query = self.expand_templ.replace("//aoid", normalized_id)
|
||||||
rows = self.db.get_rows(sql_query, True)
|
rows = self.db.get_rows(sql_query, True)
|
||||||
except Exception, err:
|
except Exception as err:
|
||||||
if BasicConfig.logging:
|
if BasicConfig.logging:
|
||||||
logging.error(traceback.format_exc(err))
|
logging.error(traceback.format_exc())
|
||||||
return dict(error=err.args[0])
|
if BasicConfig.debug_print:
|
||||||
|
traceback.print_exc()
|
||||||
|
return dict(error=str(err))
|
||||||
|
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
@ -111,9 +136,11 @@ class FiasFactory:
|
|||||||
rows = self.db.get_rows(sql_query, True)
|
rows = self.db.get_rows(sql_query, True)
|
||||||
|
|
||||||
assert len(rows), "Record with this AOID not found in DB"
|
assert len(rows), "Record with this AOID not found in DB"
|
||||||
except Exception, err:
|
except Exception as err:
|
||||||
if BasicConfig.logging:
|
if BasicConfig.logging:
|
||||||
logging.error(traceback.format_exc(err))
|
logging.error(traceback.format_exc())
|
||||||
return dict(error=err.args[0])
|
if BasicConfig.debug_print:
|
||||||
|
traceback.print_exc()
|
||||||
|
return dict(error=str(err))
|
||||||
|
|
||||||
return rows
|
return rows
|
||||||
|
@ -1,18 +1,16 @@
|
|||||||
# -*- coding: utf-8 -*-
|
import Levenshtein
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import time
|
|
||||||
|
|
||||||
import Levenshtein
|
|
||||||
import sphinxapi
|
import sphinxapi
|
||||||
|
import time
|
||||||
|
|
||||||
from aore.config import BasicConfig
|
from aore.config import BasicConfig
|
||||||
from aore.config import SphinxConfig
|
from aore.config import SphinxConfig
|
||||||
from aore.miscutils.exceptions import FiasException
|
from aore.miscutils.exceptions import FiasException
|
||||||
from aore.miscutils.fysearch import violet_ratio
|
from aore.miscutils.fysearch import violet_ratio
|
||||||
from aore.miscutils.trigram import trigram
|
from aore.miscutils.trigram import trigram
|
||||||
from wordentry import WordEntry
|
from .wordentry import WordEntry
|
||||||
from wordvariation import VariationType
|
from .wordvariation import VariationType
|
||||||
|
|
||||||
|
|
||||||
class SphinxSearch:
|
class SphinxSearch:
|
||||||
@ -62,7 +60,7 @@ class SphinxSearch:
|
|||||||
self.client_show.SetSortMode(sphinxapi.SPH_SORT_EXTENDED, "krank DESC")
|
self.client_show.SetSortMode(sphinxapi.SPH_SORT_EXTENDED, "krank DESC")
|
||||||
|
|
||||||
def __get_suggest(self, word, rating_limit, count):
|
def __get_suggest(self, word, rating_limit, count):
|
||||||
word_len = str(len(word) / 2)
|
word_len = len(word)
|
||||||
trigrammed_word = '"{}"/1'.format(trigram(word))
|
trigrammed_word = '"{}"/1'.format(trigram(word))
|
||||||
|
|
||||||
self.__configure(SphinxConfig.index_sugg, word_len)
|
self.__configure(SphinxConfig.index_sugg, word_len)
|
||||||
@ -95,7 +93,7 @@ class SphinxSearch:
|
|||||||
return outlist
|
return outlist
|
||||||
|
|
||||||
# Получает список объектов (слово)
|
# Получает список объектов (слово)
|
||||||
def __get_word_entries(self, words, strong):
|
def __get_word_entries(self, words):
|
||||||
we_list = []
|
we_list = []
|
||||||
for word in words:
|
for word in words:
|
||||||
if word != '':
|
if word != '':
|
||||||
@ -108,17 +106,16 @@ class SphinxSearch:
|
|||||||
# text - текст найденного адресного объекта
|
# text - текст найденного адресного объекта
|
||||||
# ratio - рейтинг найденного пункта
|
# ratio - рейтинг найденного пункта
|
||||||
# cort - рейтинг количества совпавших слов
|
# cort - рейтинг количества совпавших слов
|
||||||
|
|
||||||
def find(self, text, strong):
|
def find(self, text, strong):
|
||||||
def split_phrase(phrase):
|
def split_phrase(phrase):
|
||||||
phrase = unicode(phrase).lower()
|
phrase = phrase.lower()
|
||||||
return re.split(r"[ ,:.#$]+", phrase)
|
return re.split(r"[ ,:.#$]+", phrase)
|
||||||
|
|
||||||
# сплитим текст на слова
|
# сплитим текст на слова
|
||||||
words = split_phrase(text)
|
words = split_phrase(text)
|
||||||
|
|
||||||
# получаем список объектов (слов)
|
# получаем список объектов (слов)
|
||||||
word_entries = self.__get_word_entries(words, strong)
|
word_entries = self.__get_word_entries(words)
|
||||||
word_count = len(word_entries)
|
word_count = len(word_entries)
|
||||||
|
|
||||||
# проверяем, есть ли вообще что-либо в списке объектов слов (или же все убрали как частое)
|
# проверяем, есть ли вообще что-либо в списке объектов слов (или же все убрали как частое)
|
||||||
@ -169,23 +166,23 @@ class SphinxSearch:
|
|||||||
parsed_ids.append(match['attrs']['aoid'])
|
parsed_ids.append(match['attrs']['aoid'])
|
||||||
results.append(
|
results.append(
|
||||||
dict(aoid=match['attrs']['aoid'],
|
dict(aoid=match['attrs']['aoid'],
|
||||||
text=unicode(match['attrs']['fullname']),
|
text=str(match['attrs']['fullname']),
|
||||||
ratio=match['attrs']['krank'],
|
ratio=match['attrs']['krank'],
|
||||||
cort=i))
|
cort=i))
|
||||||
|
|
||||||
# При строгом поиске нам надо еще добавить fuzzy и выбрать самое большое значение при отклонении
|
# При строгом поиске нам надо еще добавить fuzzy и выбрать самое большое значение при отклонении
|
||||||
# выше заданного
|
# выше заданного, по сути переопределяем ratio
|
||||||
if strong:
|
if strong:
|
||||||
for result in results:
|
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")
|
raise FiasException("No matches")
|
||||||
else:
|
else:
|
||||||
return results[0]
|
results = [results[0]]
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from aore.config import SphinxConfig
|
from aore.config import SphinxConfig
|
||||||
from aore.search.wordvariation import WordVariation, VariationType
|
from aore.search.wordvariation import WordVariation, VariationType
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_string(word):
|
||||||
|
return word.replace('-', '').replace('@', '').replace('#', '')
|
||||||
|
|
||||||
|
|
||||||
class WordEntry:
|
class WordEntry:
|
||||||
# Варианты распеределния для слов с первыми двумя символами, где:
|
# Варианты распеределния для слов с первыми двумя символами, где:
|
||||||
# 0 - не найдено, 1 - найдено одно, x - найдено много (>1)
|
# 0 - не найдено, 1 - найдено одно, x - найдено много (>1)
|
||||||
@ -51,14 +54,14 @@ class WordEntry:
|
|||||||
|
|
||||||
def __init__(self, db, word):
|
def __init__(self, db, word):
|
||||||
self.db = db
|
self.db = db
|
||||||
self.bare_word = str(word)
|
self.bare_word = word
|
||||||
self.word = self.__cleanify(self.bare_word)
|
self.word = cleanup_string(self.bare_word)
|
||||||
self.word_len = len(unicode(self.word))
|
self.word_len = len(self.word)
|
||||||
self.parameters = dict(IS_FREQ=False, SOCR_WORD=None)
|
self.parameters = dict(IS_FREQ=False, SOCR_WORD=None)
|
||||||
self.ranks = self.__init_ranks()
|
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
|
self.__dict__[mt_name] = False
|
||||||
for mt_value in mt_values:
|
for mt_value in mt_values:
|
||||||
self.__dict__[mt_name] = self.__dict__[mt_name] or re.search(mt_value, self.ranks)
|
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_LAST_STAR = False
|
||||||
self.MT_AS_IS = True
|
self.MT_AS_IS = True
|
||||||
|
|
||||||
def __cleanify(self, word):
|
|
||||||
return word.replace('-', '').replace('@', '')
|
|
||||||
|
|
||||||
def variations_generator(self, strong, suggestion_func):
|
def variations_generator(self, strong, suggestion_func):
|
||||||
default_var_type = VariationType.normal
|
default_var_type = VariationType.normal
|
||||||
# Если слово встречается часто, ставим у всех вариантов тип VariationType.freq
|
# Если слово встречается часто, ставим у всех вариантов тип VariationType.freq
|
||||||
@ -115,8 +115,9 @@ class WordEntry:
|
|||||||
"UNION ALL SELECT COUNT(*), NULL FROM \"AOTRIG\" WHERE word='{}' " \
|
"UNION ALL SELECT COUNT(*), NULL FROM \"AOTRIG\" WHERE word='{}' " \
|
||||||
"UNION ALL SELECT COUNT(*), MAX(scname) FROM \"SOCRBASE\" WHERE socrname ILIKE '{}'" \
|
"UNION ALL SELECT COUNT(*), MAX(scname) FROM \"SOCRBASE\" WHERE socrname ILIKE '{}'" \
|
||||||
"UNION ALL SELECT COUNT(*), NULL FROM \"SOCRBASE\" WHERE scname ILIKE '{}'" \
|
"UNION ALL SELECT COUNT(*), NULL FROM \"SOCRBASE\" WHERE scname ILIKE '{}'" \
|
||||||
"UNION ALL SELECT frequency, NULL FROM \"AOTRIG\" WHERE word='{}';".format(
|
"UNION ALL SELECT frequency, NULL FROM \"AOTRIG\" WHERE word='{}';".format(self.word, self.word_len,
|
||||||
self.word, self.word_len, self.word, self.bare_word, self.bare_word, self.word)
|
self.word, self.bare_word,
|
||||||
|
self.bare_word, self.word)
|
||||||
|
|
||||||
result = self.db.get_rows(sql_qry)
|
result = self.db.get_rows(sql_qry)
|
||||||
|
|
||||||
@ -146,8 +147,5 @@ class WordEntry:
|
|||||||
def get_type(self):
|
def get_type(self):
|
||||||
return ", ".join([x for x in self.match_types if self.__dict__[x]])
|
return ", ".join([x for x in self.match_types if self.__dict__[x]])
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
return self.word
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.word)
|
return str(self.word)
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from enum import Enum # Типы вариаций слова
|
from enum import Enum # Типы вариаций слова
|
||||||
|
|
||||||
|
|
||||||
|
0
aore/static/.gitkeep
Normal file
0
aore/static/.gitkeep
Normal file
1
aore/templates/postgre/convert_query.sql
Normal file
1
aore/templates/postgre/convert_query.sql
Normal 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;
|
@ -1,4 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
import codecs
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from aore.config import Folders
|
from aore.config import Folders
|
||||||
@ -38,7 +38,7 @@ class AoDataParser:
|
|||||||
# Prepare to next iteration
|
# Prepare to next iteration
|
||||||
self.counter = 0
|
self.counter = 0
|
||||||
self.currentpage += 1
|
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()
|
exit_nodes = list()
|
||||||
for allowed_field in self.allowed_fields:
|
for allowed_field in self.allowed_fields:
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os.path
|
import os.path
|
||||||
from traceback import format_exc
|
|
||||||
|
|
||||||
import rarfile
|
import rarfile
|
||||||
import requests
|
import requests
|
||||||
|
from traceback import format_exc
|
||||||
|
|
||||||
from aore.config import Folders, UnrarConfig
|
from aore.config import Folders, UnrarConfig
|
||||||
from aore.miscutils.exceptions import FiasException
|
from aore.miscutils.exceptions import FiasException
|
||||||
from aoxmltableentry import AoXmlTableEntry
|
from .aoxmltableentry import AoXmlTableEntry
|
||||||
|
|
||||||
|
|
||||||
class AoRar:
|
class AoRar:
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
@ -22,7 +20,7 @@ class AoXmlTableEntry:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_dir(cls, file_name, path):
|
def from_dir(cls, file_name, path):
|
||||||
# for extracted into folder
|
# 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):
|
def __init__(self, file_name, lamda_open):
|
||||||
matchings = re.search('^(AS_)(DEL_)*([A-Z]+)', file_name)
|
matchings = re.search('^(AS_)(DEL_)*([A-Z]+)', file_name)
|
||||||
@ -42,8 +40,5 @@ class AoXmlTableEntry:
|
|||||||
def close(self):
|
def close(self):
|
||||||
self.file_descriptor.close()
|
self.file_descriptor.close()
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
return "Entry for {} table {}".format(self.operation_type, self.table_name)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Entry for {} table {}".format(self.operation_type, self.table_name)
|
return "Entry for {} table {}".format(self.operation_type, self.table_name)
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
from bottle import template
|
from bottle import template
|
||||||
|
|
||||||
@ -66,4 +63,3 @@ class DbHandler:
|
|||||||
|
|
||||||
self.db.execute(sql_query)
|
self.db.execute(sql_query)
|
||||||
logging.info("All indexes was deleted.")
|
logging.info("All indexes was deleted.")
|
||||||
|
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from pysimplesoap.client import SoapClient
|
from pysimplesoap.client import SoapClient
|
||||||
|
|
||||||
|
|
||||||
class SoapReceiver:
|
class SoapReceiver:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.client = SoapClient(
|
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/',
|
action='http://fias.nalog.ru/WebServices/Public/DownloadService.asmx/',
|
||||||
namespace="http://fias.nalog.ru/WebServices/Public/DownloadService.asmx",
|
namespace="http://fias.nalog.ru/WebServices/Public/DownloadService.asmx",
|
||||||
soap_ns='soap', trace=False, ns=False)
|
soap_ns='soap', trace=False, ns=False)
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from os import walk, path
|
|
||||||
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
from os import walk, path
|
||||||
|
|
||||||
from aore.config import DatabaseConfig
|
from aore.config import DatabaseConfig
|
||||||
from aore.dbutils.dbimpl import DBImpl
|
from aore.dbutils.dbimpl import DBImpl
|
||||||
@ -62,9 +59,9 @@ class Updater:
|
|||||||
mode = None
|
mode = None
|
||||||
while not mode:
|
while not mode:
|
||||||
try:
|
try:
|
||||||
mode = int(raw_input('Enter FIAS update version (3 digit):'))
|
mode = int(input('Enter FIAS update version (3 digit):'))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print "Not a valid fias version, try again."
|
print("Not a valid fias version, try again.")
|
||||||
|
|
||||||
return mode
|
return mode
|
||||||
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
|
||||||
@ -7,7 +5,8 @@ class XMLParser:
|
|||||||
def __init__(self, parse_function):
|
def __init__(self, parse_function):
|
||||||
self.parse_function = 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:
|
for event, elem in context:
|
||||||
# print event
|
# print event
|
||||||
func(elem, *args, **kwargs)
|
func(elem, *args, **kwargs)
|
||||||
|
26
config.example_lin.py
Normal file
26
config.example_lin.py
Normal 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"
|
@ -1,17 +1,17 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from aore import config
|
from aore import config
|
||||||
|
|
||||||
# Config section
|
# Config section
|
||||||
|
|
||||||
# Address and port where sphinx was listening,
|
# 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"
|
config.SphinxConfig.listen = "127.0.0.1:9312"
|
||||||
# Base sphinx folder
|
# Base sphinx folder
|
||||||
config.SphinxConfig.var_dir = "C:\\Sphinx"
|
config.SphinxConfig.var_dir = "C:\\Sphinx"
|
||||||
|
|
||||||
# DB config
|
# DB config
|
||||||
config.DatabaseConfig.database = "fias_db"
|
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.port = 5432
|
||||||
config.DatabaseConfig.user = "postgres"
|
config.DatabaseConfig.user = "postgres"
|
||||||
config.DatabaseConfig.password = "postgres"
|
config.DatabaseConfig.password = "postgres"
|
||||||
@ -22,4 +22,5 @@ config.UnrarConfig.path = "C:\\Program Files\\WinRAR\\unrar.exe"
|
|||||||
config.Folders.temp = "E:\\!TEMP"
|
config.Folders.temp = "E:\\!TEMP"
|
||||||
|
|
||||||
config.BasicConfig.logging = True
|
config.BasicConfig.logging = True
|
||||||
config.BasicConfig.logfile = "pyphias.log"
|
config.BasicConfig.debug_print = False
|
||||||
|
config.BasicConfig.logfile = "pyphias.log"
|
11
manage.py
11
manage.py
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import logging
|
import logging
|
||||||
import optparse
|
import optparse
|
||||||
import sys
|
import sys
|
||||||
@ -23,7 +22,7 @@ def print_fias_versions():
|
|||||||
all_versions = imp.get_update_list()
|
all_versions = imp.get_update_list()
|
||||||
|
|
||||||
print("Installed version: {}".format(current_version))
|
print("Installed version: {}".format(current_version))
|
||||||
print("Avaliable updates:")
|
print("Available updates:")
|
||||||
print("Number\t\tDate")
|
print("Number\t\tDate")
|
||||||
for upd in all_versions:
|
for upd in all_versions:
|
||||||
mark_current = (' ', '*')[int(upd['intver']) == current_version]
|
mark_current = (' ', '*')[int(upd['intver']) == current_version]
|
||||||
@ -60,6 +59,14 @@ def get_allowed_updates(updates_str, mode="create"):
|
|||||||
yield all_versions[-1]
|
yield all_versions[-1]
|
||||||
else:
|
else:
|
||||||
assert len(user_defined_list) == 1, "Ony single update number allowed for DB create"
|
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":
|
if mode == "update":
|
||||||
for uv in all_versions:
|
for uv in all_versions:
|
||||||
uv_ver = uv['intver']
|
uv_ver = uv['intver']
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
lxml>=3.4.0
|
lxml>=3.6.0
|
||||||
bottle>=0.12.9
|
bottle==0.12.13
|
||||||
psycopg2>=2.6.1
|
psycopg2==2.7.5
|
||||||
PySimpleSOAP==1.16
|
PySimpleSOAP==1.16.2
|
||||||
python-Levenshtein==0.12.0
|
python-Levenshtein==0.12.0
|
||||||
enum34>=1.0.0
|
rarfile==3.0
|
||||||
rarfile
|
|
||||||
requests>=2.8.1
|
requests>=2.8.1
|
||||||
soap2py==1.16
|
soap2py>=1.16
|
||||||
sphinxapi
|
git+https://github.com/jar3b/sphinx-py3-api
|
||||||
|
bottle-oop-rest>=0.0.5
|
@ -8,11 +8,12 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
assert "No config"
|
assert "No config"
|
||||||
|
|
||||||
# Define main app
|
# Create main app
|
||||||
phias_app = phias.App(config.BasicConfig.logfile)
|
phias.App(config.BasicConfig.logfile)
|
||||||
|
|
||||||
# Define wsgi app
|
# Define wsgi app
|
||||||
application = phias_app.get_app()
|
application = phias.app
|
||||||
|
|
||||||
# Run bottle WSGI server if no external
|
# Run bottle WSGI server if no external
|
||||||
if __name__ == '__main__':
|
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)
|
Loading…
x
Reference in New Issue
Block a user