Commit cfea515c authored by xa's avatar xa

first commit

parents
.direnv
.envrc
.git
.gitlab-ci.yml
Makefile
Dockerfile
README.rst
layout python3
# Created by https://www.gitignore.io/api/python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# dotenv
.env
# virtualenv
.venv
venv/
ENV/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject
# End of https://www.gitignore.io/api/python
FROM alpine:edge
MAINTAINER Xavier Barbosa <clint.northwood@gmail.com>
RUN apk add --no-cache \
ca-certificates \
file \
gcc \
g++ \
libuv \
make \
python3 \
python3-dev \
py3-aiohttp
ADD . /srv
RUN mv /srv/phone/country-codes.csv /usr/share/country-codes.csv
WORKDIR /srv
RUN python3 setup.py install
EXPOSE 80
ENV PYTHONUNBUFFERED 1
CMD ["python3", "-m", "phone", "0.0.0.0:80", "--database", "/usr/share/country-codes.csv"]
build:
docker build --pull -t errorist/phone:latest .
serve: build
docker run --rm -p 38002:80 errorist/phone:latest
GeoIP
=====
Microservice that let you validate phone numbers.
Usage::
docker run --rm -p 80:80 hub.errorist.xyz/srv/phone:latest
Once it's up, you can query it::
GET /?number=123456789&code=FR HTTP/1.1
Host: localhost:80
It will respond with::
HTTP/1.1 200 OK
Cache-Control: public, immutable, max-age=365000000
Content-Type: application/json; charset=utf-8
{
"lat": 48.8534,
"lng": 2.3488,
"city": "Paris",
"country": "Frankreich",
"iso_code": "FR",
"accuracy": 100,
"timezone": "Europe/Paris"
}
Query parameters:
* `number`: a number to check. mandatory
* `code`: a code to check, like FR, US, GB. optional
Returned fields:
* `number`: formatted number
* `timezone`: linked timezone. may be null
Returned status codes:
* `200`: Found a location for the given IP address
* `400`: Miss the IP field
* `404`: No address found for the given location
* `422`: Given data is not a valid IPv4 nor IPv6 address
* `500`: Ça chie dans la colle
It exposes also some codes::
GET /country?lang=en
Host: localhost:80
It will respond with::
HTTP/1.1 200 OK
Cache-Control: public, immutable, max-age=365000000
Content-Type: application/json; charset=utf-8
[
{"name": "Afghanistan", "dial": "93", "code": "AF"},
{"name": "Albania", "dial": "355", "code": "AL"},
...
]
Query parameters:
* `lang`: a language for translating country and city.
Supported languages are: `de`, `en`, `es`, `fr`, `ja`, `pt-BR`, `ru`, and `zh-CN`.
__version__ = '1.0.0'
import sys
from phone.main import main
main(sys.argv[1:])
This diff is collapsed.
class PossibleError(Exception):
pass
class ValidError(Exception):
pass
from collections import namedtuple
from functools import lru_cache
import phonenumbers
from phonenumbers.timezone import time_zones_for_number
from . import errors
import csv
Country = namedtuple('Country', 'name dial code')
Record = namedtuple('Record', 'number timezone')
async def init_lookup(app):
app['lookup'] = Lookup(app['config']['database'])
async def close_lookup(app):
del app['lookup']
class Lookup:
def __init__(self, path):
self.path = path
@lru_cache(maxsize=256)
def countries(self, language=None):
language = language or 'en'
with open(self.path, encoding="utf-8") as csvfile:
for row in csv.DictReader(csvfile, delimiter=',', quotechar='"'):
name = row['name'] or row['official_name_en']
dial = row['Dial']
code = row['ISO3166-1-Alpha-2']
if not name:
continue
if not dial:
continue
if not code:
continue
yield Country(name, dial, code)
@lru_cache(maxsize=256)
def __call__(self, *, number, code=None):
try:
parsed = phonenumbers.parse(number, code)
if not phonenumbers.is_possible_number(parsed):
raise errors.PossibleError('%s is not a possible number' % number)
if not phonenumbers.is_valid_number(parsed):
raise errors.ValidError('%s not valid number' % number)
except phonenumbers.NumberParseException as error:
if error.error_type == NumberParseException.INVALID_COUNTRY_CODE:
raise ParseError('country code is invalid or missing') from error
elif error.error_type == NumberParseException.NOT_A_NUMBER:
raise ParseError('%s is not a valid number' % number) from error
elif error.error_type == NumberParseException.TOO_SHORT_AFTER_IDD:
raise ParseError('%s is too short idd' % number) from error
elif error.error_type == NumberParseException.TOO_SHORT_NSN:
raise ParseError('%s is too short nsn' % number) from error
elif error.error_type == NumberParseException.TOO_LONG:
raise ParseError('%s is too long' % number) from error
raise ParseError(error._msg) from error
except Exception as error:
raise
formatted = phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)
timezone = time_zones_for_number(parsed)
return Record(formatted, timezone)
import argparse
import asyncio
import logging
import uvloop
import sys
import types
from aiohttp import web
from phone.lookup import close_lookup, init_lookup
from phone.middlewares import setup_middlewares
from phone.routes import setup_routes, PROJECT_ROOT
def parse_address(addr, *, host: str, port: int):
if addr is None:
pass
elif isinstance(addr, int):
port = addr
elif ':' in addr:
a, _, b = addr.partition(':')
host = a or host
port = b or port
elif addr.isdigit():
port = addr
elif addr:
host = addr
return str(host), int(port)
def parse_args(args=None):
parser = argparse.ArgumentParser()
parser.add_argument('listen', nargs='?')
parser.add_argument('--unix-socket', action='store_true')
parser.add_argument('--database', default=PROJECT_ROOT.joinpath('country-codes.csv'))
args = parser.parse_args(args)
if args.unix_socket:
return types.MappingProxyType({
'host': None,
'port': None,
'path': args.listen,
'database': args.database
})
host, port = parse_address(args.listen, host='localhost', port=47001)
return types.MappingProxyType({
'host': host,
'port': port,
'path': None,
'database': args.database
})
def init(loop, argv):
app = web.Application(loop=loop)
app.on_startup.append(init_lookup)
app.on_cleanup.append(close_lookup)
app['config'] = parse_args(argv)
setup_routes(app)
setup_middlewares(app)
return app
def main(argv):
logging.basicConfig(level=logging.INFO)
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
loop = asyncio.get_event_loop()
app = init(loop, argv)
web.run_app(app,
host=app['config']['host'],
port=app['config']['port'],
path=app['config']['path'])
if __name__ == '__main__':
main(sys.argv[1:])
from aiohttp import web
async def handle_500(request, response):
return web.json_response({
'error': {'code': 500, 'message': 'Internal error'}
}, status=500)
def error_pages(overrides):
async def middleware(app, handler):
async def middleware_handler(request):
try:
response = await handler(request)
override = overrides.get(response.status)
if override is None:
return response
else:
return await override(request, response)
except web.HTTPException as ex:
override = overrides.get(ex.status)
if override is None:
raise
else:
return await override(request, ex)
return middleware_handler
return middleware
def setup_middlewares(app):
error_middleware = error_pages({500: handle_500})
app.middlewares.append(error_middleware)
import pathlib
from .views import countries, validate
PROJECT_ROOT = pathlib.Path(__file__).parent
def setup_routes(app):
app.router.add_get('/', validate)
app.router.add_get('/countries', countries)
from aiohttp import web
async def countries(request):
lang = request.query.get('lang', None)
result = []
for name, dial, code in request.app['lookup'].countries(language=lang):
result.append({
'name': name,
'dial': dial,
'code': code
})
return web.json_response(result, headers={
'Cache-Control': 'public, immutable, max-age=365000000'
}, status=200)
async def validate(request):
try:
code = request.query.get('code', None)
number = request.query['number']
number, timezone = request.app['lookup'](number=number, code=code)
except KeyError:
return web.json_response({
'error': {'code': 400, 'message': 'number is mandatory'}
}, status=400)
except ValueError as e:
return web.json_response({
'error': {'code': 422, 'message': str(e)}
}, status=422)
except Exception as e:
return web.json_response({
'error': {'code': 400, 'message': str(e)}
}, status=404)
return web.json_response({
'number': number,
'timezone': timezone
}, headers={
'Cache-Control': 'public, immutable, max-age=365000000'
}, status=200)
#!/usr/bin/env python
from setuptools import setup, find_packages
setup(
name='phone',
version='1.0.0',
description='Polls project example from aiohttp',
platforms=['POSIX'],
packages=find_packages(),
package_data={
'': ['*.csv']
},
include_package_data=True,
install_requires=[
'aiohttp',
'phonenumbers',
'uvloop'],
zip_safe=False
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment