Compare commits

...
Author SHA1 Message Date
Rafa de la Torre 98d533b707 Bump version of python pip package to 0.9.1 2016-10-06 15:05:00 +02:00
Rafa de la Torre 6d0ad85d48 Fix for crash when sentinel_master_id is absent in config 2016-10-06 15:04:25 +02:00
Rafa de la Torre 00e6cace76 Update version of the python pip package 2016-10-06 11:06:28 +02:00
Rafa de la Torre e9ad35ba1d Merge pull request #284 from CartoDB/redis-refactor-take2
Redis refactor: Take 2
2016-10-05 18:32:08 +02:00
Rafa de la Torre dcb3935021 Upgrade/downgrade files for server extension 2016-10-05 18:28:24 +02:00
Rafa de la Torre cded6c2f08 Test for NullConfigStorage 2016-10-04 15:20:41 +02:00
Rafa de la Torre e1b357137a Rename s/storage/backend for consistency 2016-10-04 14:59:57 +02:00
Rafa de la Torre 3844cfc226 Tests for environment module 2016-10-04 14:56:06 +02:00
Rafa de la Torre 2a1276f4f1 Add test for redis_config module 2016-10-04 12:53:52 +02:00
Rafa de la Torre 35da7e48fd Implement a ServerConfigBackendFactory
mostly to keep layers separated.
2016-10-03 17:42:19 +02:00
Rafa de la Torre 12aebb7eee Rename ConfigStorageInterface to ConfigBackendInterface 2016-10-03 17:35:33 +02:00
Rafa de la Torre 0d87a95270 Inject the environment into the LoggerConfigBuilder 2016-10-03 17:06:57 +02:00
Rafa de la Torre 18e1a5c7c9 Remove the class User and its tests
as it is not used.
2016-10-03 16:54:40 +02:00
Rafa de la Torre fcca5da302 Add a RedisMetricsConnectionFactory class 2016-10-03 16:47:45 +02:00
Rafa de la Torre 1aec541906 Add ServerEnvironment and ServerEnvironmentBuilder 2016-10-03 16:21:28 +02:00
Rafa de la Torre 9e98e0794d Add backends for user and org configs 2016-10-03 16:05:08 +02:00
Rafa de la Torre 8fbb41742c Rename redis conf factories to builders 2016-10-03 15:25:01 +02:00
Rafa de la Torre 275a6dc27f Move redis_config related classes to separate file 2016-10-03 13:33:46 +02:00
Rafa de la Torre d522083d5c Rename redis_connection_config test file as well 2016-10-03 13:22:47 +02:00
Rafa de la Torre 073163eb1a Rename redis_config.py to redis_connection_config.py 2016-10-03 13:20:30 +02:00
Rafa de la Torre 0c62c4bada Move the NullConfigStorage to a separate file 2016-10-03 13:17:04 +02:00
Rafa de la Torre 3361960cfc Move InMemoryConfigStorage to a separate file 2016-10-03 13:12:49 +02:00
Rafa de la Torre 86ab3abc53 Rename mapzen_geocoder module to mapzen_geocoder_config 2016-10-03 12:58:15 +02:00
Rafa de la Torre b1f3405cd0 Rename variable s/user_geocoder_config/mapzen_geocoder_config 2016-10-03 12:52:18 +02:00
Rafa de la Torre fb812ee15e Add tests for redis connection configs 2016-10-03 12:33:38 +02:00
Rafa de la Torre c1dd410201 Make RedisConnectionConfigBuilder abstract
and also use custom ConfigException instead of generic one.
2016-10-03 12:21:45 +02:00
Rafa de la Torre 34ddd28e6b Move ConfigException to a separate file 2016-10-03 12:20:17 +02:00
Rafa de la Torre d85bc65bf8 Ignore coverage files 2016-10-03 12:18:47 +02:00
Carla 443fe88d5a Add newline to end fle 2016-09-30 13:08:27 +02:00
Carla Iriberri 6c61626214 Remove RedisMock TODO 2016-09-30 12:29:00 +02:00
Carla Iriberri 74d2fba763 Add Redis Mock -- tested and working 2016-09-30 12:28:27 +02:00
Rafa de la Torre e24819f193 Take the environment into account 2016-09-30 11:29:44 +02:00
Rafa de la Torre 1e6ee8d5c1 Add an Environment class 2016-09-30 11:21:35 +02:00
Rafa de la Torre 3a6cc4c364 Add mapzen config and integrate into legacy code 2016-09-29 18:41:16 +02:00
Rafa de la Torre 8ad2434b1d Add instantiation of configs to mapzen geocoder 2016-09-29 17:47:36 +02:00
Rafa de la Torre 0b7b44d8a5 Add a couple of factories to abstract user/org configs 2016-09-29 17:46:17 +02:00
Rafa de la Torre 02a2619b45 Fix instantiation of redis metadata connection 2016-09-29 17:19:18 +02:00
Rafa de la Torre 4b4a02905c Implement the RedisConfigStorage 2016-09-29 17:18:45 +02:00
Rafa de la Torre 1f3a655ae5 Add all the code to instantiate a user_config_storage (WIP) 2016-09-29 16:57:31 +02:00
Rafa de la Torre 9d60fde0b8 A stubb for RedisConfigStorage (WIP) 2016-09-29 13:07:40 +02:00
Rafa de la Torre efdc151282 Add things to get logger config from storage 2016-09-28 18:22:51 +02:00
Rafa de la Torre fd2cc21942 Add server config storage classes 2016-09-28 17:32:26 +02:00
Rafa de la Torre 18f05fbd4f User class with tests 2016-09-28 16:30:37 +02:00
Carla d2f4586bae Merge pull request #283 from CartoDB/master
Getting master back to develop: Update NEWS.md for new Python release
2016-09-28 15:53:59 +02:00
Carla 54eb279ae8 Update NEWS.md 2016-09-28 15:40:25 +02:00
Carla 85d6c2a54e Update NEWS.md 2016-09-28 15:38:48 +02:00
Carla cad2051efe Merge pull request #282 from CartoDB/development
Release Python server package version 0.8.1
2016-09-28 15:37:58 +02:00
Carla Iriberri 96a93e3c56 Python package version 0.8.1 2016-09-28 15:09:29 +02:00
Rafa de la Torre facda9e8be Merge pull request #275 from CartoDB/fix_qps_retry
Fixed QPS retry decorator
2016-09-28 13:47:45 +02:00
Rafa de la Torre 64fc18b9e0 Fix and improve test speed 2016-09-28 13:43:50 +02:00
Mario de Frutos 9381d5644b Fixed QPS retry decorator 2016-09-16 13:46:02 +02:00
Carla 9f55f2ee3b Update README.md 2016-09-09 11:44:57 +02:00
Carla 1087c1266b Merge pull request #272 from CartoDB/development
Release client 0.11.1
2016-09-08 12:42:26 +02:00
Carla d5a296a30c Update NEWS.md 2016-09-08 10:39:48 +02:00
Carla Iriberri f8caf4314d Release client 0.11.1 2016-09-07 17:52:53 +02:00
Carla d7910fbbf1 Merge pull request #271 from CartoDB/new_framework_obs_compat
Use real function name for compatibility with observatory
2016-09-07 17:45:32 +02:00
Carla d47049c813 Escape characters in example 2016-09-07 16:01:45 +02:00
Carla cc8f93c535 Use real function name for compatibility 2016-09-07 15:40:20 +02:00
Carla 3f9441de7e Merge pull request #269 from CartoDB/development
New release client 0.11.0 and server  0.15.2
2016-09-02 12:20:24 +02:00
64 changed files with 5601 additions and 49 deletions
+1
View File
@@ -1,5 +1,6 @@
.DS_Store .DS_Store
*.pyc *.pyc
.coverage
cartodb_services.egg-info/ cartodb_services.egg-info/
build/ build/
dist/ dist/
+12
View File
@@ -1,3 +1,15 @@
September 28, 2016
==========
* Released version 0.8.1 of Python package cartodb\_services
* Improvements in QPS retry decorator for requests to external services
https://github.com/CartoDB/dataservices-api/releases/tag/python-0.8.1
September 8, 2016
===========
* Released version 0.11.1 of the client
* Minor change in the name of the function parameter sent to server and Observatory backend for compatibility with the last observatory-extension framework updates
September 1, 2016 September 1, 2016
=========== ===========
* Released version 0.11.0 of the client * Released version 0.11.0 of the client
+1 -1
View File
@@ -5,7 +5,7 @@ The CARTO Data Services SQL API
Steps to deploy a new Data Services API version : Steps to deploy a new Data Services API version :
- Deploy new version of dataservices API to all servers - Deploy new version of dataservices API to all servers
- Update the server user using: ALTER EXTENSION cdb_dataservices_server UPDATE TO '<CURRENT_VERSION>'; - Update the server user using: ALTER EXTENSION cdb_dataservices_server UPDATE TO '\<CURRENT_VERSION\>';
- Update the python dependencies if needed: **cartodb_geocoder** and **heremaps** - Update the python dependencies if needed: **cartodb_geocoder** and **heremaps**
- Add the needed config in the `cdb_conf` table: - Add the needed config in the `cdb_conf` table:
- `redis_metadata_config` and `redis_metrics_conf` - `redis_metadata_config` and `redis_metrics_conf`
+2 -2
View File
@@ -13,8 +13,8 @@ OLD_VERSIONS = $(wildcard old_versions/*.sql)
# @see http://www.postgresql.org/docs/current/static/extend-pgxs.html # @see http://www.postgresql.org/docs/current/static/extend-pgxs.html
DATA = $(NEW_EXTENSION_ARTIFACT) \ DATA = $(NEW_EXTENSION_ARTIFACT) \
$(OLD_VERSIONS) \ $(OLD_VERSIONS) \
cdb_dataservices_client--0.10.2--0.11.0.sql \ cdb_dataservices_client--0.11.0--0.11.1.sql \
cdb_dataservices_client--0.11.0--0.10.2.sql cdb_dataservices_client--0.11.1--0.11.0.sql
REGRESS = $(notdir $(basename $(wildcard test/sql/*test.sql))) REGRESS = $(notdir $(basename $(wildcard test/sql/*test.sql)))
@@ -0,0 +1,140 @@
--DO NOT MODIFY THIS FILE, IT IS GENERATED AUTOMATICALLY FROM SOURCES
-- Complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "ALTER EXTENSION cdb_dataservices_client UPDATE TO '0.11.1'" to load this file. \quit
CREATE OR REPLACE FUNCTION cdb_dataservices_client.__DST_PrepareTableOBS_GetMeasure(
username text,
orgname text,
user_db_role text,
user_schema text,
output_table_name text,
params json
) RETURNS boolean AS $$
function_name = 'OBS_GetMeasure'
# Obtain return types for augmentation procedure
ds_return_metadata = plpy.execute("SELECT colnames, coltypes "
"FROM cdb_dataservices_client._DST_GetReturnMetadata({username}::text, {orgname}::text, {function_name}::text, {params}::json);"
.format(
username=plpy.quote_nullable(username),
orgname=plpy.quote_nullable(orgname),
function_name=plpy.quote_literal(function_name),
params=plpy.quote_literal(params)
)
)
if ds_return_metadata[0]["colnames"]:
colnames_arr = ds_return_metadata[0]["colnames"]
coltypes_arr = ds_return_metadata[0]["coltypes"]
else:
raise Exception('Error retrieving OBS_GetMeasure metadata')
# Prepare column and type strings required in the SQL queries
columns_with_types_arr = [colnames_arr[i] + ' ' + coltypes_arr[i] for i in range(0,len(colnames_arr))]
columns_with_types = ','.join(columns_with_types_arr)
# Create a new table with the required columns
plpy.execute('CREATE TABLE "{schema}".{table_name} ( '
'cartodb_id int, the_geom geometry, {columns_with_types} '
');'
.format(schema=user_schema, table_name=output_table_name, columns_with_types=columns_with_types)
)
plpy.execute('ALTER TABLE "{schema}".{table_name} OWNER TO "{user}";'
.format(schema=user_schema, table_name=output_table_name, user=user_db_role)
)
return True
$$ LANGUAGE plpythonu;
CREATE OR REPLACE FUNCTION cdb_dataservices_client.__DST_PopulateTableOBS_GetMeasure(
username text,
orgname text,
user_db_role text,
user_schema text,
dbname text,
table_name text,
output_table_name text,
params json
) RETURNS boolean AS $$
function_name = 'OBS_GetMeasure'
# Obtain return types for augmentation procedure
ds_return_metadata = plpy.execute(
"SELECT colnames, coltypes "
"FROM cdb_dataservices_client._DST_GetReturnMetadata({username}::text, {orgname}::text, {function_name}::text, {params}::json);" .format(
username=plpy.quote_nullable(username),
orgname=plpy.quote_nullable(orgname),
function_name=plpy.quote_literal(function_name),
params=plpy.quote_literal(params)))
if ds_return_metadata[0]["colnames"]:
colnames_arr = ds_return_metadata[0]["colnames"]
coltypes_arr = ds_return_metadata[0]["coltypes"]
else:
raise Exception('Error retrieving OBS_GetMeasure metadata')
# Prepare column and type strings required in the SQL queries
columns_with_types_arr = [
colnames_arr[i] +
' ' +
coltypes_arr[i] for i in range(
0,
len(colnames_arr))]
columns_with_types = ','.join(columns_with_types_arr)
aliased_colname_list = ','.join(
['result.' + name for name in colnames_arr])
# Instruct the OBS server side to establish a FDW
# The metadata is obtained as well in order to:
# - (a) be able to write the query to grab the actual data to be executed in the remote server via pl/proxy,
# - (b) be able to tell OBS to free resources when done.
ds_fdw_metadata = plpy.execute(
"SELECT schemaname, tabname, servername "
"FROM cdb_dataservices_client._DST_ConnectUserTable({username}::text, {orgname}::text, {user_db_role}::text, "
"{schema}::text, {dbname}::text, {table_name}::text);" .format(
username=plpy.quote_nullable(username),
orgname=plpy.quote_nullable(orgname),
user_db_role=plpy.quote_literal(user_db_role),
schema=plpy.quote_literal(user_schema),
dbname=plpy.quote_literal(dbname),
table_name=plpy.quote_literal(table_name)))
if ds_fdw_metadata[0]["schemaname"]:
server_schema = ds_fdw_metadata[0]["schemaname"]
server_table_name = ds_fdw_metadata[0]["tabname"]
server_name = ds_fdw_metadata[0]["servername"]
else:
raise Exception('Error connecting dataset via FDW')
# Create a new table with the required columns
plpy.execute(
'INSERT INTO "{schema}".{analysis_table_name} '
'SELECT ut.cartodb_id, ut.the_geom, {colname_list} '
'FROM "{schema}".{table_name} ut '
'LEFT JOIN _DST_FetchJoinFdwTableData({username}::text, {orgname}::text, {server_schema}::text, {server_table_name}::text, '
'{function_name}::text, {params}::json) '
'AS result ({columns_with_types}, cartodb_id int) '
'ON result.cartodb_id = ut.cartodb_id;' .format(
schema=user_schema,
analysis_table_name=output_table_name,
colname_list=aliased_colname_list,
table_name=table_name,
username=plpy.quote_nullable(username),
orgname=plpy.quote_nullable(orgname),
server_schema=plpy.quote_literal(server_schema),
server_table_name=plpy.quote_literal(server_table_name),
function_name=plpy.quote_literal(function_name),
params=plpy.quote_literal(params),
columns_with_types=columns_with_types))
# Wipe user FDW data from the server
wiped = plpy.execute(
"SELECT cdb_dataservices_client._DST_DisconnectUserTable({username}::text, {orgname}::text, {server_schema}::text, "
"{server_table_name}::text, {fdw_server}::text)" .format(
username=plpy.quote_nullable(username),
orgname=plpy.quote_nullable(orgname),
server_schema=plpy.quote_literal(server_schema),
server_table_name=plpy.quote_literal(server_table_name),
fdw_server=plpy.quote_literal(server_name)))
return True
$$ LANGUAGE plpythonu;
@@ -0,0 +1,140 @@
--DO NOT MODIFY THIS FILE, IT IS GENERATED AUTOMATICALLY FROM SOURCES
-- Complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "ALTER EXTENSION cdb_dataservices_client UPDATE TO '0.11.0'" to load this file. \quit
CREATE OR REPLACE FUNCTION cdb_dataservices_client.__DST_PrepareTableOBS_GetMeasure(
username text,
orgname text,
user_db_role text,
user_schema text,
output_table_name text,
params json
) RETURNS boolean AS $$
function_name = 'GetMeasure'
# Obtain return types for augmentation procedure
ds_return_metadata = plpy.execute("SELECT colnames, coltypes "
"FROM cdb_dataservices_client._DST_GetReturnMetadata({username}::text, {orgname}::text, {function_name}::text, {params}::json);"
.format(
username=plpy.quote_nullable(username),
orgname=plpy.quote_nullable(orgname),
function_name=plpy.quote_literal(function_name),
params=plpy.quote_literal(params)
)
)
if ds_return_metadata[0]["colnames"]:
colnames_arr = ds_return_metadata[0]["colnames"]
coltypes_arr = ds_return_metadata[0]["coltypes"]
else:
raise Exception('Error retrieving OBS_GetMeasure metadata')
# Prepare column and type strings required in the SQL queries
columns_with_types_arr = [colnames_arr[i] + ' ' + coltypes_arr[i] for i in range(0,len(colnames_arr))]
columns_with_types = ','.join(columns_with_types_arr)
# Create a new table with the required columns
plpy.execute('CREATE TABLE "{schema}".{table_name} ( '
'cartodb_id int, the_geom geometry, {columns_with_types} '
');'
.format(schema=user_schema, table_name=output_table_name, columns_with_types=columns_with_types)
)
plpy.execute('ALTER TABLE "{schema}".{table_name} OWNER TO "{user}";'
.format(schema=user_schema, table_name=output_table_name, user=user_db_role)
)
return True
$$ LANGUAGE plpythonu;
CREATE OR REPLACE FUNCTION cdb_dataservices_client.__DST_PopulateTableOBS_GetMeasure(
username text,
orgname text,
user_db_role text,
user_schema text,
dbname text,
table_name text,
output_table_name text,
params json
) RETURNS boolean AS $$
function_name = 'GetMeasure'
# Obtain return types for augmentation procedure
ds_return_metadata = plpy.execute(
"SELECT colnames, coltypes "
"FROM cdb_dataservices_client._DST_GetReturnMetadata({username}::text, {orgname}::text, {function_name}::text, {params}::json);" .format(
username=plpy.quote_nullable(username),
orgname=plpy.quote_nullable(orgname),
function_name=plpy.quote_literal(function_name),
params=plpy.quote_literal(params)))
if ds_return_metadata[0]["colnames"]:
colnames_arr = ds_return_metadata[0]["colnames"]
coltypes_arr = ds_return_metadata[0]["coltypes"]
else:
raise Exception('Error retrieving OBS_GetMeasure metadata')
# Prepare column and type strings required in the SQL queries
columns_with_types_arr = [
colnames_arr[i] +
' ' +
coltypes_arr[i] for i in range(
0,
len(colnames_arr))]
columns_with_types = ','.join(columns_with_types_arr)
aliased_colname_list = ','.join(
['result.' + name for name in colnames_arr])
# Instruct the OBS server side to establish a FDW
# The metadata is obtained as well in order to:
# - (a) be able to write the query to grab the actual data to be executed in the remote server via pl/proxy,
# - (b) be able to tell OBS to free resources when done.
ds_fdw_metadata = plpy.execute(
"SELECT schemaname, tabname, servername "
"FROM cdb_dataservices_client._DST_ConnectUserTable({username}::text, {orgname}::text, {user_db_role}::text, "
"{schema}::text, {dbname}::text, {table_name}::text);" .format(
username=plpy.quote_nullable(username),
orgname=plpy.quote_nullable(orgname),
user_db_role=plpy.quote_literal(user_db_role),
schema=plpy.quote_literal(user_schema),
dbname=plpy.quote_literal(dbname),
table_name=plpy.quote_literal(table_name)))
if ds_fdw_metadata[0]["schemaname"]:
server_schema = ds_fdw_metadata[0]["schemaname"]
server_table_name = ds_fdw_metadata[0]["tabname"]
server_name = ds_fdw_metadata[0]["servername"]
else:
raise Exception('Error connecting dataset via FDW')
# Create a new table with the required columns
plpy.execute(
'INSERT INTO "{schema}".{analysis_table_name} '
'SELECT ut.cartodb_id, ut.the_geom, {colname_list} '
'FROM "{schema}".{table_name} ut '
'LEFT JOIN _DST_FetchJoinFdwTableData({username}::text, {orgname}::text, {server_schema}::text, {server_table_name}::text, '
'{function_name}::text, {params}::json) '
'AS result ({columns_with_types}, cartodb_id int) '
'ON result.cartodb_id = ut.cartodb_id;' .format(
schema=user_schema,
analysis_table_name=output_table_name,
colname_list=aliased_colname_list,
table_name=table_name,
username=plpy.quote_nullable(username),
orgname=plpy.quote_nullable(orgname),
server_schema=plpy.quote_literal(server_schema),
server_table_name=plpy.quote_literal(server_table_name),
function_name=plpy.quote_literal(function_name),
params=plpy.quote_literal(params),
columns_with_types=columns_with_types))
# Wipe user FDW data from the server
wiped = plpy.execute(
"SELECT cdb_dataservices_client._DST_DisconnectUserTable({username}::text, {orgname}::text, {server_schema}::text, "
"{server_table_name}::text, {fdw_server}::text)" .format(
username=plpy.quote_nullable(username),
orgname=plpy.quote_nullable(orgname),
server_schema=plpy.quote_literal(server_schema),
server_table_name=plpy.quote_literal(server_table_name),
fdw_server=plpy.quote_literal(server_name)))
return True
$$ LANGUAGE plpythonu;
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,5 +1,5 @@
comment = 'CartoDB dataservices client API extension' comment = 'CartoDB dataservices client API extension'
default_version = '0.11.0' default_version = '0.11.1'
requires = 'plproxy, cartodb' requires = 'plproxy, cartodb'
superuser = true superuser = true
schema = cdb_dataservices_client schema = cdb_dataservices_client
+2 -2
View File
@@ -100,7 +100,7 @@ CREATE OR REPLACE FUNCTION cdb_dataservices_client.__DST_PrepareTableOBS_GetMeas
output_table_name text, output_table_name text,
params json params json
) RETURNS boolean AS $$ ) RETURNS boolean AS $$
function_name = 'GetMeasure' function_name = 'OBS_GetMeasure'
# Obtain return types for augmentation procedure # Obtain return types for augmentation procedure
ds_return_metadata = plpy.execute("SELECT colnames, coltypes " ds_return_metadata = plpy.execute("SELECT colnames, coltypes "
"FROM cdb_dataservices_client._DST_GetReturnMetadata({username}::text, {orgname}::text, {function_name}::text, {params}::json);" "FROM cdb_dataservices_client._DST_GetReturnMetadata({username}::text, {orgname}::text, {function_name}::text, {params}::json);"
@@ -146,7 +146,7 @@ CREATE OR REPLACE FUNCTION cdb_dataservices_client.__DST_PopulateTableOBS_GetMea
output_table_name text, output_table_name text,
params json params json
) RETURNS boolean AS $$ ) RETURNS boolean AS $$
function_name = 'GetMeasure' function_name = 'OBS_GetMeasure'
# Obtain return types for augmentation procedure # Obtain return types for augmentation procedure
ds_return_metadata = plpy.execute( ds_return_metadata = plpy.execute(
"SELECT colnames, coltypes " "SELECT colnames, coltypes "
@@ -0,0 +1,69 @@
--DO NOT MODIFY THIS FILE, IT IS GENERATED AUTOMATICALLY FROM SOURCES
-- Complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "ALTER EXTENSION cdb_dataservices_server UPDATE TO '0.16.0'" to load this file. \quit
-- Here goes your code to upgrade/downgrade
-- This is done in order to avoid an undesired depedency on cartodb extension
CREATE OR REPLACE FUNCTION cdb_dataservices_server.cdb_conf_getconf(input_key text)
RETURNS JSON AS $$
SELECT VALUE FROM cartodb.cdb_conf WHERE key = input_key;
$$ LANGUAGE SQL STABLE SECURITY DEFINER;
CREATE OR REPLACE FUNCTION cdb_dataservices_server._cdb_mapzen_geocode_street_point(username TEXT, orgname TEXT, searchtext TEXT, city TEXT DEFAULT NULL, state_province TEXT DEFAULT NULL, country TEXT DEFAULT NULL)
RETURNS Geometry AS $$
import cartodb_services
cartodb_services.init(plpy, GD)
from cartodb_services.mapzen import MapzenGeocoder
from cartodb_services.mapzen.types import country_to_iso3
from cartodb_services.metrics import QuotaService
from cartodb_services.tools import Logger
from cartodb_services.refactor.tools.logger import LoggerConfigBuilder
from cartodb_services.refactor.service.mapzen_geocoder_config import MapzenGeocoderConfigBuilder
from cartodb_services.refactor.core.environment import ServerEnvironmentBuilder
from cartodb_services.refactor.backend.server_config import ServerConfigBackendFactory
from cartodb_services.refactor.backend.user_config import UserConfigBackendFactory
from cartodb_services.refactor.backend.org_config import OrgConfigBackendFactory
from cartodb_services.refactor.backend.redis_metrics_connection import RedisMetricsConnectionFactory
server_config_backend = ServerConfigBackendFactory().get()
environment = ServerEnvironmentBuilder(server_config_backend).get()
user_config_backend = UserConfigBackendFactory(username, environment, server_config_backend).get()
org_config_backend = OrgConfigBackendFactory(orgname, environment, server_config_backend).get()
logger_config = LoggerConfigBuilder(environment, server_config_backend).get()
logger = Logger(logger_config)
mapzen_geocoder_config = MapzenGeocoderConfigBuilder(server_config_backend, user_config_backend, org_config_backend, username, orgname).get()
redis_metrics_connection = RedisMetricsConnectionFactory(environment, server_config_backend).get()
quota_service = QuotaService(mapzen_geocoder_config, redis_metrics_connection)
if not quota_service.check_user_quota():
raise Exception('You have reached the limit of your quota')
try:
geocoder = MapzenGeocoder(mapzen_geocoder_config.mapzen_api_key, logger)
country_iso3 = None
if country:
country_iso3 = country_to_iso3(country)
coordinates = geocoder.geocode(searchtext=searchtext, city=city,
state_province=state_province,
country=country_iso3, search_type='address')
if coordinates:
quota_service.increment_success_service_use()
plan = plpy.prepare("SELECT ST_SetSRID(ST_MakePoint($1, $2), 4326); ", ["double precision", "double precision"])
point = plpy.execute(plan, [coordinates[0], coordinates[1]], 1)[0]
return point['st_setsrid']
else:
quota_service.increment_empty_service_use()
return None
except BaseException as e:
import sys
quota_service.increment_failed_service_use()
logger.error('Error trying to geocode street point using mapzen', sys.exc_info(), data={"username": username, "orgname": orgname})
raise Exception('Error trying to geocode street point using mapzen')
finally:
quota_service.increment_total_service_use()
$$ LANGUAGE plpythonu;
@@ -0,0 +1,54 @@
--DO NOT MODIFY THIS FILE, IT IS GENERATED AUTOMATICALLY FROM SOURCES
-- Complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "ALTER EXTENSION cdb_dataservices_server UPDATE TO '0.15.1'" to load this file. \quit
-- Here goes your code to upgrade/downgrade
DROP FUNCTION IF EXISTS cdb_dataservices_server.cdb_conf_getconf(text);
-- Geocodes a street address given a searchtext and a state and/or country
CREATE OR REPLACE FUNCTION cdb_dataservices_server.cdb_geocode_street_point(username TEXT, orgname TEXT, searchtext TEXT, city TEXT DEFAULT NULL, state_province TEXT DEFAULT NULL, country TEXT DEFAULT NULL)
RETURNS Geometry AS $$
import cartodb_services
cartodb_services.init(plpy, GD)
from cartodb_services.config.user import User
from cartodb_services.config.configs import ConfigsFactory
from cartodb_services.config.hires_geocoder_config import HiResGeocoderConfigFactory
from cartodb_services.request.request import RequestFactory
user = User(username, orgname)
configs = ConfigsFactory.get(user)
request = RequestFactory().create(user, configs, 'cdb_geocode_street_point')
# TODO change to hires_geocoder_config = HiResGeocoderConfigFactory.get(request)
hires_geocoder_config = HiResGeocoderConfigFactory(configs).get(user)
if hires_geocoder_config.provider == 'here':
here_plan = plpy.prepare("SELECT cdb_dataservices_server._cdb_here_geocode_street_point($1, $2, $3, $4, $5, $6) as point; ", ["text", "text", "text", "text", "text", "text"])
return plpy.execute(here_plan, [username, orgname, searchtext, city, state_province, country], 1)[0]['point']
elif hires_geocoder_config.provider == 'google':
google_plan = plpy.prepare("SELECT cdb_dataservices_server._cdb_google_geocode_street_point($1, $2, $3, $4, $5, $6) as point; ", ["text", "text", "text", "text", "text", "text"])
return plpy.execute(google_plan, [username, orgname, searchtext, city, state_province, country], 1)[0]['point']
elif hires_geocoder_config.provider == 'mapzen':
mapzen_plan = plpy.prepare("SELECT cdb_dataservices_server._cdb_mapzen_geocode_street_point($1, $2, $3, $4, $5, $6) as point; ", ["text", "text", "text", "text", "text", "text"])
return plpy.execute(mapzen_plan, [username, orgname, searchtext, city, state_province, country], 1)[0]['point']
else:
raise Exception('Requested geocoder is not available')
$$ LANGUAGE plpythonu;
CREATE OR REPLACE FUNCTION cdb_dataservices_server.cdb_here_geocode_street_point(username TEXT, orgname TEXT, searchtext TEXT, city TEXT DEFAULT NULL, state_province TEXT DEFAULT NULL, country TEXT DEFAULT NULL)
RETURNS Geometry AS $$
plpy.execute("SELECT cdb_dataservices_server._connect_to_redis('{0}')".format(username))
redis_conn = GD["redis_connection_{0}".format(username)]['redis_metrics_connection']
plpy.execute("SELECT cdb_dataservices_server._get_geocoder_config({0}, {1})".format(plpy.quote_nullable(username), plpy.quote_nullable(orgname)))
user_geocoder_config = GD["user_geocoder_config_{0}".format(username)]
if user_geocoder_config.heremaps_geocoder:
here_plan = plpy.prepare("SELECT cdb_dataservices_server._cdb_here_geocode_street_point($1, $2, $3, $4, $5, $6) as point; ", ["text", "text", "text", "text", "text", "text"])
return plpy.execute(here_plan, [username, orgname, searchtext, city, state_province, country], 1)[0]['point']
else:
raise Exception('Here geocoder is not available for your account.')
$$ LANGUAGE plpythonu;
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
comment = 'CartoDB dataservices server extension' comment = 'CartoDB dataservices server extension'
default_version = '0.15.1' default_version = '0.16.0'
requires = 'plpythonu, plproxy, postgis, cdb_geocoder' requires = 'plpythonu, plproxy, postgis, cdb_geocoder'
superuser = true superuser = true
schema = cdb_dataservices_server schema = cdb_dataservices_server
@@ -10,6 +10,12 @@ RETURNS boolean AS $$
return True return True
$$ LANGUAGE plpythonu SECURITY DEFINER; $$ LANGUAGE plpythonu SECURITY DEFINER;
-- This is done in order to avoid an undesired depedency on cartodb extension
CREATE OR REPLACE FUNCTION cdb_dataservices_server.cdb_conf_getconf(input_key text)
RETURNS JSON AS $$
SELECT VALUE FROM cartodb.cdb_conf WHERE key = input_key;
$$ LANGUAGE SQL STABLE SECURITY DEFINER;
CREATE OR REPLACE FUNCTION cdb_dataservices_server._get_geocoder_config(username text, orgname text, provider text DEFAULT NULL) CREATE OR REPLACE FUNCTION cdb_dataservices_server._get_geocoder_config(username text, orgname text, provider text DEFAULT NULL)
RETURNS boolean AS $$ RETURNS boolean AS $$
cache_key = "user_geocoder_config_{0}".format(username) cache_key = "user_geocoder_config_{0}".format(username)
+22 -7
View File
@@ -137,23 +137,38 @@ $$ LANGUAGE plpythonu;
CREATE OR REPLACE FUNCTION cdb_dataservices_server._cdb_mapzen_geocode_street_point(username TEXT, orgname TEXT, searchtext TEXT, city TEXT DEFAULT NULL, state_province TEXT DEFAULT NULL, country TEXT DEFAULT NULL) CREATE OR REPLACE FUNCTION cdb_dataservices_server._cdb_mapzen_geocode_street_point(username TEXT, orgname TEXT, searchtext TEXT, city TEXT DEFAULT NULL, state_province TEXT DEFAULT NULL, country TEXT DEFAULT NULL)
RETURNS Geometry AS $$ RETURNS Geometry AS $$
import cartodb_services
cartodb_services.init(plpy, GD)
from cartodb_services.mapzen import MapzenGeocoder from cartodb_services.mapzen import MapzenGeocoder
from cartodb_services.mapzen.types import country_to_iso3 from cartodb_services.mapzen.types import country_to_iso3
from cartodb_services.metrics import QuotaService from cartodb_services.metrics import QuotaService
from cartodb_services.tools import Logger,LoggerConfig from cartodb_services.tools import Logger
from cartodb_services.refactor.tools.logger import LoggerConfigBuilder
from cartodb_services.refactor.service.mapzen_geocoder_config import MapzenGeocoderConfigBuilder
from cartodb_services.refactor.core.environment import ServerEnvironmentBuilder
from cartodb_services.refactor.backend.server_config import ServerConfigBackendFactory
from cartodb_services.refactor.backend.user_config import UserConfigBackendFactory
from cartodb_services.refactor.backend.org_config import OrgConfigBackendFactory
from cartodb_services.refactor.backend.redis_metrics_connection import RedisMetricsConnectionFactory
redis_conn = GD["redis_connection_{0}".format(username)]['redis_metrics_connection'] server_config_backend = ServerConfigBackendFactory().get()
user_geocoder_config = GD["user_geocoder_config_{0}".format(username)] environment = ServerEnvironmentBuilder(server_config_backend).get()
user_config_backend = UserConfigBackendFactory(username, environment, server_config_backend).get()
org_config_backend = OrgConfigBackendFactory(orgname, environment, server_config_backend).get()
plpy.execute("SELECT cdb_dataservices_server._get_logger_config()") logger_config = LoggerConfigBuilder(environment, server_config_backend).get()
logger_config = GD["logger_config"]
logger = Logger(logger_config) logger = Logger(logger_config)
quota_service = QuotaService(user_geocoder_config, redis_conn)
mapzen_geocoder_config = MapzenGeocoderConfigBuilder(server_config_backend, user_config_backend, org_config_backend, username, orgname).get()
redis_metrics_connection = RedisMetricsConnectionFactory(environment, server_config_backend).get()
quota_service = QuotaService(mapzen_geocoder_config, redis_metrics_connection)
if not quota_service.check_user_quota(): if not quota_service.check_user_quota():
raise Exception('You have reached the limit of your quota') raise Exception('You have reached the limit of your quota')
try: try:
geocoder = MapzenGeocoder(user_geocoder_config.mapzen_api_key, logger) geocoder = MapzenGeocoder(mapzen_geocoder_config.mapzen_api_key, logger)
country_iso3 = None country_iso3 = None
if country: if country:
country_iso3 = country_to_iso3(country) country_iso3 = country_to_iso3(country)
+1 -1
View File
@@ -1,4 +1,4 @@
# CartoDB dataservices API python module # CARTO dataservices API python module
This directory contains the python library used by the server side of CARTO LDS (Location Data Services). This directory contains the python library used by the server side of CARTO LDS (Location Data Services).
@@ -0,0 +1,35 @@
# NOTE: This init function must be called from plpythonu entry points to
# initialize cartodb_services module properly. E.g:
#
# CREATE OR REPLACE FUNCTION cdb_dataservices_server.cdb_isochrone(...)
# RETURNS SETOF cdb_dataservices_server.isoline AS $$
#
# import cartodb_services
# cartodb_services.init(plpy, GD)
#
# # rest of the code here
# cartodb_services.GD[key] = val
# cartodb_services.plpy.execute('SELECT * FROM ...')
#
# $$ LANGUAGE plpythonu;
plpy = None
GD = None
def init(_plpy, _GD):
global plpy
global GD
if plpy is None:
plpy = _plpy
if GD is None:
GD = _GD
def _reset():
# NOTE: just for testing
global plpy
global GD
plpy = None
GD = None
@@ -19,3 +19,15 @@ class MalformedResult(Exception):
class TimeoutException(Exception): class TimeoutException(Exception):
def __str__(self): def __str__(self):
return repr('Timeout requesting to mapzen server') return repr('Timeout requesting to mapzen server')
class ServiceException(Exception):
def __init__(self, message, response):
self.message = message
self.response = response
def response(self):
return self.response
def __str__(self):
return self.message
@@ -2,7 +2,7 @@ import requests
import json import json
import re import re
from exceptions import WrongParams, MalformedResult from exceptions import WrongParams, MalformedResult, ServiceException
from qps import qps_retry from qps import qps_retry
from cartodb_services.tools import Coordinate, PolyLine from cartodb_services.tools import Coordinate, PolyLine
@@ -17,8 +17,9 @@ class MapzenGeocoder:
self._url = base_url self._url = base_url
self._logger = logger self._logger = logger
@qps_retry @qps_retry(qps=20)
def geocode(self, searchtext, city=None, state_province=None, country=None, search_type=None): def geocode(self, searchtext, city=None, state_province=None,
country=None, search_type=None):
request_params = self._build_requests_parameters(searchtext, city, request_params = self._build_requests_parameters(searchtext, city,
state_province, state_province,
country, search_type) country, search_type)
@@ -31,21 +32,21 @@ class MapzenGeocoder:
else: else:
self._logger.error('Error trying to geocode using mapzen', self._logger.error('Error trying to geocode using mapzen',
data={"response_status": response.status_code, data={"response_status": response.status_code,
"response_reason": response.reason, "response_reason": response.reason,
"response_content": response.text, "response_content": response.text,
"reponse_url": response.url, "reponse_url": response.url,
"response_headers": response.headers, "response_headers": response.headers,
"searchtext": searchtext, "searchtext": searchtext,
"city": city, "country": country, "city": city, "country": country,
"state_province": state_province }) "state_province": state_province})
raise Exception('Error trying to geocode {0} using mapzen'.format(searchtext)) raise ServiceException('Error trying to geocode {0} using mapzen'.format(searchtext),
response)
except requests.ConnectionError as e: except requests.ConnectionError as e:
# Don't raise the exception to continue with the geocoding job # Don't raise the exception to continue with the geocoding job
self._logger.error('Error connecting to Mapzen geocoding server', self._logger.error('Error connecting to Mapzen geocoding server',
exception=e) exception=e)
return [] return []
def _build_requests_parameters(self, searchtext, city=None, def _build_requests_parameters(self, searchtext, city=None,
state_province=None, country=None, state_province=None, country=None,
search_type=None): search_type=None):
@@ -1,6 +1,7 @@
import requests import requests
import json import json
from qps import qps_retry from qps import qps_retry
from exceptions import ServiceException
class MatrixClient: class MatrixClient:
@@ -51,6 +52,6 @@ class MatrixClient:
"response_headers": response.headers, "response_headers": response.headers,
"locations": locations, "locations": locations,
"costing": costing}) "costing": costing})
raise Exception('Error trying to get matrix distance from mapzen') raise ServiceException("Error trying to get matrix distance from mapzen", response)
return response.json() return response.json()
@@ -4,18 +4,38 @@ from datetime import datetime
from exceptions import TimeoutException from exceptions import TimeoutException
DEFAULT_RETRY_TIMEOUT = 60 DEFAULT_RETRY_TIMEOUT = 60
DEFAULT_QUERIES_PER_SECOND = 10
def qps_retry(original_function=None,**options):
def qps_retry(f): """ Query Per Second retry decorator
def wrapped_f(*args, **kw): The intention of this decorator is to retry requests against third
return QPSService().call(f, *args, **kw) party services that has QPS restriction.
return wrapped_f Parameters:
- timeout: Maximum number of seconds to retry
- qps: Allowed queries per second. This parameter is used to
calculate the next time to retry the request
"""
if original_function is not None:
def wrapped_function(*args, **kwargs):
if 'timeout' in options:
timeout = options['timeout']
else:
timeout = DEFAULT_RETRY_TIMEOUT
if 'qps' in options:
qps = options['qps']
else:
qps = DEFAULT_QUERIES_PER_SECOND
return QPSService(retry_timeout=timeout, queries_per_second=qps).call(original_function, *args, **kwargs)
return wrapped_function
else:
def partial_wrapper(func):
return qps_retry(func, **options)
return partial_wrapper
class QPSService: class QPSService:
def __init__(self, queries_per_second=10, def __init__(self, queries_per_second, retry_timeout):
retry_timeout=DEFAULT_RETRY_TIMEOUT):
self._queries_per_second = queries_per_second self._queries_per_second = queries_per_second
self._retry_timeout = retry_timeout self._retry_timeout = retry_timeout
@@ -27,7 +47,7 @@ class QPSService:
return fn(*args, **kwargs) return fn(*args, **kwargs)
except Exception as e: except Exception as e:
response = getattr(e, 'response', None) response = getattr(e, 'response', None)
if response and (response.status_code == 429): if response is not None and (response.status_code == 429):
self.retry(start_time, attempt_number) self.retry(start_time, attempt_number)
else: else:
raise e raise e
@@ -35,7 +55,7 @@ class QPSService:
def retry(self, first_request_time, retry_count): def retry(self, first_request_time, retry_count):
elapsed = datetime.now() - first_request_time elapsed = datetime.now() - first_request_time
if elapsed.seconds > self._retry_timeout: if elapsed.microseconds > (self._retry_timeout * 1000.0):
raise TimeoutException() raise TimeoutException()
# inverse qps * (1.5 ^ i) is an increased sleep time of 1.5x per # inverse qps * (1.5 ^ i) is an increased sleep time of 1.5x per
@@ -2,7 +2,7 @@ import requests
import json import json
import re import re
from exceptions import WrongParams, MalformedResult from exceptions import WrongParams, MalformedResult, ServiceException
from qps import qps_retry from qps import qps_retry
from cartodb_services.tools import Coordinate, PolyLine from cartodb_services.tools import Coordinate, PolyLine
@@ -57,7 +57,7 @@ class MapzenRouting:
"response_headers": response.headers, "response_headers": response.headers,
"waypoints": waypoints, "mode": mode, "waypoints": waypoints, "mode": mode,
"options": options}) "options": options})
raise Exception('Error trying to calculate route using Mapzen') raise ServiceException('Error trying to calculate route using Mapzen', response)
def __parse_options(self, options): def __parse_options(self, options):
return dict(option.split('=') for option in options) return dict(option.split('=') for option in options)
@@ -0,0 +1,24 @@
from cartodb_services.refactor.storage.redis_connection_config import RedisMetadataConnectionConfigBuilder
from cartodb_services.refactor.storage.redis_connection import RedisConnectionBuilder
from cartodb_services.refactor.storage.redis_config import RedisOrgConfigStorageBuilder
class OrgConfigBackendFactory(object):
"""
This class abstracts the creation of an org configuration backend. It will return
an implementation of the ConfigBackendInterface appropriate to the org, depending
on the environment.
"""
def __init__(self, orgname, environment, server_config_backend):
self._orgname = orgname
self._environment = environment
self._server_config_backend = server_config_backend
def get(self):
if self._environment.is_onpremise:
org_config_backend = self._server_config_backend
else:
redis_metadata_connection_config = RedisMetadataConnectionConfigBuilder(self._server_config_backend).get()
redis_metadata_connection = RedisConnectionBuilder(redis_metadata_connection_config).get()
org_config_backend = RedisOrgConfigStorageBuilder(redis_metadata_connection, self._orgname).get()
return org_config_backend
@@ -0,0 +1,17 @@
from cartodb_services.refactor.tools.redis_mock import RedisConnectionMock
from cartodb_services.refactor.storage.redis_connection_config import RedisMetricsConnectionConfigBuilder
from cartodb_services.refactor.storage.redis_connection import RedisConnectionBuilder
class RedisMetricsConnectionFactory(object):
def __init__(self, environment, server_config_storage):
self._environment = environment
self._server_config_storage = server_config_storage
def get(self):
if self._environment.is_onpremise:
redis_metrics_connection = RedisConnectionMock()
else:
redis_metrics_connection_config = RedisMetricsConnectionConfigBuilder(self._server_config_storage).get()
redis_metrics_connection = RedisConnectionBuilder(redis_metrics_connection_config).get()
return redis_metrics_connection
@@ -0,0 +1,13 @@
from cartodb_services.refactor.storage.server_config import InDbServerConfigStorage
class ServerConfigBackendFactory(object):
"""
This class creates a backend to retrieve server configurations (implementing the ConfigBackendInterface).
At this moment it will always return an InDbServerConfigStorage, but nothing prevents from changing the
implementation. To something that reads from a file, memory or whatever. It is mostly there to keep
the layers separated.
"""
def get(self):
return InDbServerConfigStorage()
@@ -0,0 +1,24 @@
from cartodb_services.refactor.storage.redis_connection_config import RedisMetadataConnectionConfigBuilder
from cartodb_services.refactor.storage.redis_connection import RedisConnectionBuilder
from cartodb_services.refactor.storage.redis_config import RedisUserConfigStorageBuilder
class UserConfigBackendFactory(object):
"""
This class abstracts the creation of a user configuration backend. It will return
an implementation of the ConfigBackendInterface appropriate to the user, depending
on the environment.
"""
def __init__(self, username, environment, server_config_backend):
self._username = username
self._environment = environment
self._server_config_backend = server_config_backend
def get(self):
if self._environment.is_onpremise:
user_config_backend = self._server_config_backend
else:
redis_metadata_connection_config = RedisMetadataConnectionConfigBuilder(self._server_config_backend).get()
redis_metadata_connection = RedisConnectionBuilder(redis_metadata_connection_config).get()
user_config_backend = RedisUserConfigStorageBuilder(redis_metadata_connection, self._username).get()
return user_config_backend
@@ -0,0 +1,2 @@
class ConfigException(Exception):
pass
@@ -0,0 +1,57 @@
class ServerEnvironment(object):
DEVELOPMENT = 'development'
STAGING = 'staging'
PRODUCTION = 'production'
ONPREMISE = 'onpremise'
VALID_ENVIRONMENTS = [
DEVELOPMENT,
STAGING,
PRODUCTION,
ONPREMISE
]
def __init__(self, environment_str):
assert environment_str in self.VALID_ENVIRONMENTS
self._environment_str = environment_str
def __str__(self):
return self._environment_str
@property
def is_development(self):
return self._environment_str == self.DEVELOPMENT
@property
def is_staging(self):
return self._environment_str == self.STAGING
@property
def is_production(self):
return self._environment_str == self.PRODUCTION
@property
def is_onpremise(self):
return self._environment_str == self.ONPREMISE
def __eq__(self, other):
return self._environment_str == other._environment_str
class ServerEnvironmentBuilder(object):
DEFAULT_ENVIRONMENT = ServerEnvironment.DEVELOPMENT
def __init__(self, server_config_storage):
self._server_config_storage = server_config_storage
def get(self):
server_config = self._server_config_storage.get('server_conf')
if not server_config or 'environment' not in server_config:
environment_str = self.DEFAULT_ENVIRONMENT
else:
environment_str = server_config['environment']
return ServerEnvironment(environment_str)
@@ -0,0 +1,11 @@
import abc
class ConfigBackendInterface(object):
"""This is an interface that all config backends must abide to"""
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def get(self, key):
"""Return a value based on the key supplied from some storage"""
pass
@@ -0,0 +1,112 @@
from dateutil.parser import parse as date_parse
class MapzenGeocoderConfig(object):
"""
Value object that represents the configuration needed to operate the mapzen service.
"""
def __init__(self,
geocoding_quota,
soft_geocoding_limit,
period_end_date,
cost_per_hit,
log_path,
mapzen_api_key,
username,
organization):
self._geocoding_quota = geocoding_quota
self._soft_geocoding_limit = soft_geocoding_limit
self._period_end_date = period_end_date
self._cost_per_hit = cost_per_hit
self._log_path = log_path
self._mapzen_api_key = mapzen_api_key
self._username = username
self._organization = organization
# Kind of generic properties. Note which ones are for actually running the
# service and which ones are needed for quota stuff.
@property
def service_type(self):
return 'geocoder_mapzen'
@property
def provider(self):
return 'mapzen'
@property
def is_high_resolution(self):
return True
@property
def geocoding_quota(self):
return self._geocoding_quota
@property
def soft_geocoding_limit(self):
return self._soft_geocoding_limit
@property
def period_end_date(self):
return self._period_end_date
@property
def cost_per_hit(self):
return self._cost_per_hit
# Server config, TODO: locate where this is actually used
@property
def log_path(self):
return self._log_path
# This is actually the specific one to run requests against the remote endpoitn
@property
def mapzen_api_key(self):
return self._mapzen_api_key
# These two identify the user
@property
def username(self):
return self._username
@property
def organization(self):
return self._organization
# TODO: for BW compat, remove
@property
def google_geocoder(self):
return False
class MapzenGeocoderConfigBuilder(object):
def __init__(self, server_conf, user_conf, org_conf, username, orgname):
self._server_conf = server_conf
self._user_conf = user_conf
self._org_conf = org_conf
self._username = username
self._orgname = orgname
def get(self):
mapzen_server_conf = self._server_conf.get('mapzen_conf')
geocoding_quota = mapzen_server_conf['geocoder']['monthly_quota']
mapzen_api_key = mapzen_server_conf['geocoder']['api_key']
soft_geocoding_limit = self._user_conf.get('soft_geocoding_limit')
cost_per_hit=0
period_end_date_str = self._org_conf.get('period_end_date') or self._user_conf.get('period_end_date')
period_end_date = date_parse(period_end_date_str)
logger_conf = self._server_conf.get('logger_conf')
log_path = logger_conf['geocoder_log_path']
return MapzenGeocoderConfig(geocoding_quota,
soft_geocoding_limit,
period_end_date,
cost_per_hit,
log_path,
mapzen_api_key,
self._username,
self._orgname)
@@ -0,0 +1,12 @@
from ..core.interfaces import ConfigBackendInterface
class InMemoryConfigStorage(ConfigBackendInterface):
def __init__(self, config_hash={}):
self._config_hash = config_hash
def get(self, key):
try:
return self._config_hash[key]
except KeyError:
return None
@@ -0,0 +1,6 @@
from ..core.interfaces import ConfigBackendInterface
class NullConfigStorage(ConfigBackendInterface):
def get(self, key):
return None
@@ -0,0 +1,36 @@
from ..core.interfaces import ConfigBackendInterface
from null_config import NullConfigStorage
class RedisConfigStorage(ConfigBackendInterface):
def __init__(self, connection, config_key):
self._connection = connection
self._config_key = config_key
self._data = None
def get(self, key):
if not self._data:
self._data = self._connection.hgetall(self._config_key)
return self._data[key]
class RedisUserConfigStorageBuilder(object):
def __init__(self, redis_connection, username):
self._redis_connection = redis_connection
self._username = username
def get(self):
return RedisConfigStorage(self._redis_connection, 'rails:users:{0}'.format(self._username))
class RedisOrgConfigStorageBuilder(object):
def __init__(self, redis_connection, orgname):
self._redis_connection = redis_connection
self._orgname = orgname
def get(self):
if self._orgname:
return RedisConfigStorage(self._redis_connection, 'rails:orgs:{0}'.format(self._orgname))
else:
return NullConfigStorage()
@@ -0,0 +1,22 @@
from redis.sentinel import Sentinel
from redis import StrictRedis
class RedisConnectionBuilder():
def __init__(self, connection_config):
self._config = connection_config
def get(self):
if self._config.sentinel_id:
sentinel = Sentinel([(self._config.host,
self._config.port)],
socket_timeout=self._config.timeout)
return sentinel.master_for(self._config.sentinel_id,
socket_timeout=self._config.timeout,
db=self._config.db,
retry_on_timeout=True)
else:
conn = StrictRedis(host=self._config.host, port=self._config.port,
db=self._config.db, retry_on_timeout=True,
socket_timeout=self._config.timeout)
return conn
@@ -0,0 +1,80 @@
from cartodb_services.refactor.config.exceptions import ConfigException
from abc import ABCMeta, abstractmethod
class RedisConnectionConfig(object):
"""
This represents a value object to contain configuration needed to set up
a connection to a redis server.
"""
def __init__(self, host, port, timeout, db, sentinel_id):
self._host = host
self._port = port
self._timeout = timeout
self._db = db
self._sentinel_id = sentinel_id
@property
def host(self):
return self._host
@property
def port(self):
return self._port
@property
def timeout(self):
return self._timeout
@property
def db(self):
return self._db
@property
def sentinel_id(self):
return self._sentinel_id
class RedisConnectionConfigBuilder(object):
__metaclass__ = ABCMeta
DEFAULT_USER_DB = 5
DEFAULT_TIMEOUT = 1.5 # seconds
@abstractmethod
def __init__(self, server_config_storage, config_key):
self._server_config_storage = server_config_storage
self._config_key = config_key
def get(self):
conf = self._server_config_storage.get(self._config_key)
if conf is None:
raise ConfigException("There is no redis configuration defined")
host = conf['redis_host']
port = conf['redis_port']
timeout = conf.get('timeout', self.DEFAULT_TIMEOUT) or self.DEFAULT_TIMEOUT
db = conf.get('redis_db', self.DEFAULT_USER_DB) or self.DEFAULT_USER_DB
sentinel_id = conf.get('sentinel_master_id', None)
return RedisConnectionConfig(host, port, timeout, db, sentinel_id)
class RedisMetadataConnectionConfigBuilder(RedisConnectionConfigBuilder):
def __init__(self, server_config_storage):
super(RedisMetadataConnectionConfigBuilder, self).__init__(
server_config_storage,
'redis_metadata_config'
)
class RedisMetricsConnectionConfigBuilder(RedisConnectionConfigBuilder):
def __init__(self, server_config_storage):
super(RedisMetricsConnectionConfigBuilder, self).__init__(
server_config_storage,
'redis_metrics_config'
)
@@ -0,0 +1,14 @@
import json
import cartodb_services
from ..core.interfaces import ConfigBackendInterface
class InDbServerConfigStorage(ConfigBackendInterface):
def get(self, key):
sql = "SELECT cdb_dataservices_server.cdb_conf_getconf('{0}') as conf".format(key)
rows = cartodb_services.plpy.execute(sql, 1)
json_output = rows[0]['conf']
if json_output:
return json.loads(json_output)
else:
return None
@@ -0,0 +1,52 @@
from cartodb_services.refactor.config.exceptions import ConfigException
class LoggerConfig(object):
"""This class is a value object needed to setup a Logger"""
def __init__(self, server_environment, rollbar_api_key, log_file_path, min_log_level):
self._server_environment = server_environment
self._rollbar_api_key = rollbar_api_key
self._log_file_path = log_file_path
self._min_log_level = min_log_level
@property
def environment(self):
return self._server_environment
@property
def rollbar_api_key(self):
return self._rollbar_api_key
@property
def log_file_path(self):
return self._log_file_path
@property
def min_log_level(self):
return self._min_log_level
# TODO this needs tests
class LoggerConfigBuilder(object):
def __init__(self, environment, server_config_storage):
self._server_environment = environment
self._server_config_storage = server_config_storage
def get(self):
logger_conf = self._server_config_storage.get('logger_conf')
if not logger_conf:
raise ConfigException('Logger configuration missing')
rollbar_api_key = self._get_value_or_none(logger_conf, 'rollbar_api_key')
log_file_path = self._get_value_or_none(logger_conf, 'log_file_path')
min_log_level = self._get_value_or_none(logger_conf, 'min_log_level') or 'warning'
logger_config = LoggerConfig(str(self._server_environment), rollbar_api_key, log_file_path, min_log_level)
return logger_config
def _get_value_or_none(self, logger_conf, key):
value = None
if key in logger_conf:
value = logger_conf[key]
return value
@@ -0,0 +1,8 @@
class RedisConnectionMock(object):
""" Simple class to mock a dummy behaviour for Redis related functions """
def zscore(self, redis_prefix, day):
pass
def zincrby(self, redis_prefix, day, amount):
pass
@@ -1,4 +1,3 @@
import plpy
import rollbar import rollbar
import logging import logging
import json import json
@@ -6,7 +5,14 @@ import traceback
import sys import sys
# Monkey patch because plpython sys module doesn't have argv and rollbar # Monkey patch because plpython sys module doesn't have argv and rollbar
# package use it # package use it
sys.__dict__['argv'] = [] if 'argv' not in sys.__dict__:
sys.__dict__['argv'] = []
# Only can be imported when is called from PLPython
try:
import plpy
except ImportError:
pass
class Logger: class Logger:
@@ -30,30 +36,28 @@ class Logger:
return return
self._send_to_rollbar('debug', text, exception, data) self._send_to_rollbar('debug', text, exception, data)
self._send_to_log_file('debug', text, exception, data) self._send_to_log_file('debug', text, exception, data)
plpy.debug(text) self._send_to_plpy('debug', text)
def info(self, text, exception=None, data={}): def info(self, text, exception=None, data={}):
if not self._check_min_level('info'): if not self._check_min_level('info'):
return return
self._send_to_rollbar('info', text, exception, data) self._send_to_rollbar('info', text, exception, data)
self._send_to_log_file('info', text, exception, data) self._send_to_log_file('info', text, exception, data)
plpy.info(text) self._send_to_plpy('info', text)
def warning(self, text, exception=None, data={}): def warning(self, text, exception=None, data={}):
if not self._check_min_level('warning'): if not self._check_min_level('warning'):
return return
self._send_to_rollbar('warning', text, exception, data) self._send_to_rollbar('warning', text, exception, data)
self._send_to_log_file('warning', text, exception, data) self._send_to_log_file('warning', text, exception, data)
plpy.warning(text) self._send_to_plpy('warning', text)
def error(self, text, exception=None, data={}): def error(self, text, exception=None, data={}):
if not self._check_min_level('error'): if not self._check_min_level('error'):
return return
self._send_to_rollbar('error', text, exception, data) self._send_to_rollbar('error', text, exception, data)
self._send_to_log_file('error', text, exception, data) self._send_to_log_file('error', text, exception, data)
# Plpy.error and fatal raises exceptions and we only want to log an self._send_to_plpy('error', text)
# error, exceptions should be raise explicitly
plpy.warning(text)
def _check_min_level(self, level): def _check_min_level(self, level):
return True if self.LEVELS[level] >= self._min_level else False return True if self.LEVELS[level] >= self._min_level else False
@@ -82,6 +86,19 @@ class Logger:
elif level == 'error': elif level == 'error':
self._file_logger.error(text, extra=extra_data) self._file_logger.error(text, extra=extra_data)
def _send_to_plpy(self, level, text):
if self._check_plpy():
if level == 'debug':
plpy.debug(text)
elif level == 'info':
plpy.info(text)
elif level == 'warning':
plpy.warning(text)
elif level == 'error':
# Plpy.error and fatal raises exceptions and we only want to
# log an error, exceptions should be raise explicitly
plpy.warning(text)
def _parse_log_extra_data(self, exception, data): def _parse_log_extra_data(self, exception, data):
extra_data = {} extra_data = {}
if exception: if exception:
@@ -118,6 +135,13 @@ class Logger:
def _log_file_activated(self): def _log_file_activated(self):
return True if self._config.log_file_path else False return True if self._config.log_file_path else False
def _check_plpy(self):
try:
module = sys.modules['plpy']
return True
except KeyError:
return False
class ConfigException(Exception): class ConfigException(Exception):
pass pass
+1 -1
View File
@@ -10,7 +10,7 @@ from setuptools import setup, find_packages
setup( setup(
name='cartodb_services', name='cartodb_services',
version='0.8', version='0.9.1',
description='CartoDB Services API Python Library', description='CartoDB Services API Python Library',
@@ -0,0 +1,47 @@
from unittest import TestCase
from cartodb_services.refactor.core.environment import *
from nose.tools import raises
from cartodb_services.refactor.storage.mem_config import InMemoryConfigStorage
class TestServerEnvironment(TestCase):
def test_can_be_a_valid_one(self):
env_dev = ServerEnvironment('development')
env_staging = ServerEnvironment('staging')
env_prod = ServerEnvironment('production')
env_onpremise = ServerEnvironment('onpremise')
@raises(AssertionError)
def test_cannot_be_a_non_valid_one(self):
env_whatever = ServerEnvironment('whatever')
def test_is_on_premise_returns_true_when_onpremise(self):
assert ServerEnvironment('onpremise').is_onpremise == True
def test_is_on_premise_returns_true_when_any_other(self):
assert ServerEnvironment('development').is_onpremise == False
assert ServerEnvironment('staging').is_onpremise == False
assert ServerEnvironment('production').is_onpremise == False
def test_equality(self):
assert ServerEnvironment('development') == ServerEnvironment('development')
assert ServerEnvironment('development') <> ServerEnvironment('onpremise')
class TestServerEnvironmentBuilder(TestCase):
def test_returns_env_according_to_configuration(self):
server_config_storage = InMemoryConfigStorage({
'server_conf': {
'environment': 'staging'
}
})
server_env = ServerEnvironmentBuilder(server_config_storage).get()
assert server_env.is_staging == True
def test_returns_default_when_no_server_conf(self):
server_config_storage = InMemoryConfigStorage({})
server_env = ServerEnvironmentBuilder(server_config_storage).get()
assert server_env.is_development == True
assert str(server_env) == ServerEnvironmentBuilder.DEFAULT_ENVIRONMENT
@@ -0,0 +1,12 @@
from unittest import TestCase
from cartodb_services.refactor.storage.mem_config import InMemoryConfigStorage
class TestInMemoryConfigStorage(TestCase):
def test_can_provide_values_from_hash(self):
server_config = InMemoryConfigStorage({'any_key': 'any_value'})
assert server_config.get('any_key') == 'any_value'
def test_gets_none_if_cannot_retrieve_key(self):
server_config = InMemoryConfigStorage()
assert server_config.get('any_non_existing_key') == None
@@ -0,0 +1,14 @@
from unittest import TestCase
from cartodb_services.refactor.storage.null_config import NullConfigStorage
from cartodb_services.refactor.core.interfaces import ConfigBackendInterface
class TestNullConfigStorage(TestCase):
def test_is_a_config_backend(self):
null_config = NullConfigStorage()
assert isinstance(null_config, ConfigBackendInterface)
def test_returns_none_regardless_of_input(self):
null_config = NullConfigStorage()
assert null_config.get('whatever') is None
@@ -0,0 +1,77 @@
from unittest import TestCase
from cartodb_services.refactor.storage.redis_config import *
from mockredis import MockRedis
from mock import Mock, MagicMock
from nose.tools import raises
class TestRedisConfigStorage(TestCase):
CONFIG_HASH_KEY = 'mykey'
def test_can_get_a_config_field(self):
connection = MockRedis()
connection.hset(self.CONFIG_HASH_KEY, 'field1', 42)
redis_config = RedisConfigStorage(connection, self.CONFIG_HASH_KEY)
value = redis_config.get('field1')
assert type(value) == str # this is something to take into account, redis always returns strings
assert value == '42'
@raises(KeyError)
def test_raises_an_exception_if_config_key_not_present(self):
connection = MockRedis()
redis_config = RedisConfigStorage(connection, self.CONFIG_HASH_KEY)
redis_config.get('whatever_field')
@raises(KeyError)
def test_returns_nothing_if_field_not_present(self):
connection = MockRedis()
connection.hmset(self.CONFIG_HASH_KEY, {'field1': 42, 'field2': 43})
redis_config = RedisConfigStorage(connection, self.CONFIG_HASH_KEY)
redis_config.get('whatever_field')
def test_it_reads_the_config_hash_just_once(self):
connection = Mock()
connection.hgetall = MagicMock(return_value={'field1': '42'})
redis_config = RedisConfigStorage(connection, self.CONFIG_HASH_KEY)
assert redis_config.get('field1') == '42'
assert redis_config.get('field1') == '42'
connection.hgetall.assert_called_once_with(self.CONFIG_HASH_KEY)
class TestRedisUserConfigStorageBuilder(TestCase):
USERNAME = 'john'
EXPECTED_REDIS_CONFIG_HASH_KEY = 'rails:users:john'
def test_it_reads_the_correct_hash_key(self):
connection = Mock()
connection.hgetall = MagicMock(return_value={'an_user_config_field': 'nice'})
redis_config = RedisConfigStorage(connection, self.EXPECTED_REDIS_CONFIG_HASH_KEY)
redis_config = RedisUserConfigStorageBuilder(connection, self.USERNAME).get()
assert redis_config.get('an_user_config_field') == 'nice'
connection.hgetall.assert_called_once_with(self.EXPECTED_REDIS_CONFIG_HASH_KEY)
class TestRedisOrgConfigStorageBuilder(TestCase):
ORGNAME = 'smith'
EXPECTED_REDIS_CONFIG_HASH_KEY = 'rails:orgs:smith'
def test_it_reads_the_correct_hash_key(self):
connection = Mock()
connection.hgetall = MagicMock(return_value={'an_org_config_field': 'awesome'})
redis_config = RedisConfigStorage(connection, self.EXPECTED_REDIS_CONFIG_HASH_KEY)
redis_config = RedisOrgConfigStorageBuilder(connection, self.ORGNAME).get()
assert redis_config.get('an_org_config_field') == 'awesome'
connection.hgetall.assert_called_once_with(self.EXPECTED_REDIS_CONFIG_HASH_KEY)
def test_it_returns_a_null_config_storage_if_theres_no_orgname(self):
redis_config = RedisOrgConfigStorageBuilder(None, None).get()
assert type(redis_config) == NullConfigStorage
assert redis_config.get('whatever') == None
@@ -0,0 +1,115 @@
from unittest import TestCase
from cartodb_services.refactor.storage.redis_connection_config import *
from cartodb_services.refactor.storage.mem_config import InMemoryConfigStorage
from cartodb_services.refactor.config.exceptions import ConfigException
class TestRedisConnectionConfig(TestCase):
def test_config_holds_values(self):
# this is mostly for completeness, dummy class, dummy test
config = RedisConnectionConfig('myhost.com', 6379, 0.1, 5, None)
assert config.host == 'myhost.com'
assert config.port == 6379
assert config.timeout == 0.1
assert config.db == 5
assert config.sentinel_id is None
class TestRedisConnectionConfigBuilder(TestCase):
def test_it_raises_exception_as_it_is_abstract(self):
server_config_storage = InMemoryConfigStorage()
self.assertRaises(TypeError, RedisConnectionConfigBuilder, server_config_storage, 'whatever_key')
class TestRedisMetadataConnectionConfigBuilder(TestCase):
def test_it_raises_exception_if_config_is_missing(self):
server_config_storage = InMemoryConfigStorage()
config_builder = RedisMetadataConnectionConfigBuilder(server_config_storage)
self.assertRaises(ConfigException, config_builder.get)
def test_it_gets_a_valid_config_from_the_server_storage(self):
server_config_storage = InMemoryConfigStorage({
'redis_metadata_config': {
'redis_host': 'myhost.com',
'redis_port': 6379,
'timeout': 0.2,
'redis_db': 3,
'sentinel_master_id': None
}
})
config = RedisMetadataConnectionConfigBuilder(server_config_storage).get()
assert config.host == 'myhost.com'
assert config.port == 6379
assert config.timeout == 0.2
assert config.db == 3
assert config.sentinel_id is None
def test_it_gets_a_default_timeout_if_none(self):
server_config_storage = InMemoryConfigStorage({
'redis_metadata_config': {
'redis_host': 'myhost.com',
'redis_port': 6379,
'timeout': None,
'redis_db': 3,
'sentinel_master_id': None
}
})
config = RedisMetadataConnectionConfigBuilder(server_config_storage).get()
assert config.host == 'myhost.com'
assert config.port == 6379
assert config.timeout == RedisConnectionConfigBuilder.DEFAULT_TIMEOUT
assert config.db == 3
assert config.sentinel_id is None
def test_it_gets_a_default_db_if_none(self):
server_config_storage = InMemoryConfigStorage({
'redis_metadata_config': {
'redis_host': 'myhost.com',
'redis_port': 6379,
'timeout': 0.2,
'redis_db': None,
'sentinel_master_id': None
}
})
config = RedisMetadataConnectionConfigBuilder(server_config_storage).get()
assert config.host == 'myhost.com'
assert config.port == 6379
assert config.timeout == 0.2
assert config.db == RedisConnectionConfigBuilder.DEFAULT_USER_DB
assert config.sentinel_id is None
class TestRedisMetricsConnectionConfigBuilder(TestCase):
def test_it_gets_a_valid_config_from_the_server_storage(self):
server_config_storage = InMemoryConfigStorage({
'redis_metrics_config': {
'redis_host': 'myhost.com',
'redis_port': 6379,
'timeout': 0.2,
'redis_db': 3,
'sentinel_master_id': 'some_master_id'
}
})
config = RedisMetricsConnectionConfigBuilder(server_config_storage).get()
assert config.host == 'myhost.com'
assert config.port == 6379
assert config.timeout == 0.2
assert config.db == 3
assert config.sentinel_id == 'some_master_id'
def test_it_sets_absent_values_to_none_or_defaults(self):
server_config_storage = InMemoryConfigStorage({
'redis_metrics_config': {
'redis_host': 'myhost.com',
'redis_port': 6379,
}
})
config = RedisMetricsConnectionConfigBuilder(server_config_storage).get()
assert config.host == 'myhost.com'
assert config.port == 6379
assert config.timeout == 1.5
assert config.db == 5
assert config.sentinel_id is None
@@ -0,0 +1,31 @@
from unittest import TestCase
from mock import Mock, MagicMock
from nose.tools import raises
from cartodb_services.refactor.storage.server_config import *
import cartodb_services
class TestInDbServerConfigStorage(TestCase):
def setUp(self):
self.plpy_mock = Mock()
cartodb_services.init(self.plpy_mock, _GD={})
def tearDown(self):
cartodb_services._reset()
def test_gets_configs_from_db(self):
self.plpy_mock.execute = MagicMock(return_value=[{'conf': '"any_value"'}])
server_config = InDbServerConfigStorage()
assert server_config.get('any_config') == 'any_value'
self.plpy_mock.execute.assert_called_once_with("SELECT cdb_dataservices_server.cdb_conf_getconf('any_config') as conf", 1)
def test_gets_none_if_cannot_retrieve_key(self):
self.plpy_mock.execute = MagicMock(return_value=[{'conf': None}])
server_config = InDbServerConfigStorage()
assert server_config.get('any_non_existing_key') is None
def test_deserializes_from_db_to_plain_dict(self):
self.plpy_mock.execute = MagicMock(return_value=[{'conf': '{"environment": "testing"}'}])
server_config = InDbServerConfigStorage()
assert server_config.get('server_conf') == {'environment': 'testing'}
self.plpy_mock.execute.assert_called_once_with("SELECT cdb_dataservices_server.cdb_conf_getconf('server_conf') as conf", 1)
@@ -6,7 +6,7 @@ import requests_mock
from mock import Mock from mock import Mock
from cartodb_services.mapzen import MapzenGeocoder from cartodb_services.mapzen import MapzenGeocoder
from cartodb_services.mapzen.exceptions import MalformedResult from cartodb_services.mapzen.exceptions import MalformedResult, TimeoutException
requests_mock.Mocker.TEST_PREFIX = 'test_' requests_mock.Mocker.TEST_PREFIX = 'test_'
@@ -0,0 +1,33 @@
import test_helper
import requests
from unittest import TestCase
from nose.tools import assert_raises
from datetime import datetime, date
from cartodb_services.mapzen.qps import qps_retry
from cartodb_services.mapzen.exceptions import ServiceException, TimeoutException
import requests_mock
import mock
requests_mock.Mocker.TEST_PREFIX = 'test_'
@requests_mock.Mocker()
class TestQPS(TestCase):
QPS_ERROR_MESSAGE = "Queries per second exceeded: Queries exceeded (10 allowed)"
def test_qps_timeout(self, req_mock):
class TestClass:
@qps_retry(timeout=0.001, qps=100)
def test(self):
response = requests.get('http://localhost/test_qps')
if response.status_code == 429:
raise ServiceException('Error 429', response)
def _text_cb(request, context):
context.status_code = 429
return self.QPS_ERROR_MESSAGE
req_mock.register_uri('GET', 'http://localhost/test_qps',
text=_text_cb)
with self.assertRaises(TimeoutException):
c = TestClass()
c.test()