26 Commits

Author SHA1 Message Date
717d2bc7d8 add helper 2021-07-28 18:00:50 +02:00
f33d4f4d25 improve generate script 2021-07-28 17:23:35 +02:00
e9aeeb12cc cli 2021-07-07 13:47:07 +02:00
e80739d7fc comment 2021-07-07 13:34:44 +02:00
81b900374e testdata and fix 2021-07-07 13:25:40 +02:00
9cabfdd484 order of statements 2021-07-07 13:07:51 +02:00
7b539fc81f touch files first 2021-07-07 13:05:30 +02:00
284eb77bfe generated 2021-07-07 13:02:16 +02:00
b6c33534f1 rental objects 2021-07-05 18:27:59 +02:00
67c97ed4bc add tenant, fix 2021-07-05 12:04:22 +02:00
81ba2655b3 add tenant 2021-07-05 12:03:56 +02:00
1e9d22483c tenant 2021-07-05 10:25:08 +02:00
daca7eca9b db refactoring, one and many 2021-07-02 14:52:07 +02:00
32a66dc189 dbconfig 2021-07-02 11:08:55 +02:00
3dd00d6dcd db in account 2021-07-02 11:02:08 +02:00
44b58bc48c db in account 2021-07-02 10:53:42 +02:00
1bddff4a23 add first get method 2021-07-01 19:37:24 +02:00
b500b81d80 fix ci script 2021-07-01 16:21:25 +02:00
5937c99eb4 change testoutput format 2021-06-17 19:10:18 +02:00
dfc5c8421a change testoutput format 2021-06-17 19:05:35 +02:00
b30e587c34 change testoutput format 2021-06-17 19:01:12 +02:00
3d0e602ee6 adjust header of openapi.yml, fix 2021-06-17 12:52:05 +02:00
30aa514495 adjust header of openapi.yml, fix 2021-06-17 12:34:25 +02:00
c72d9bc5ae adjust header of openapi.yml, fix 2021-06-17 12:32:30 +02:00
b64d04c45a adjust header of openapi.yml 2021-06-17 12:30:07 +02:00
f826248b69 fix 2021-06-17 12:07:20 +02:00
15 changed files with 492 additions and 48 deletions

5
.gitignore vendored
View File

@ -1,2 +1,7 @@
__pycache__/ __pycache__/
ENV ENV
# generated files
create.sql
methods.py
openapi.yaml

View File

@ -37,8 +37,6 @@ build:
docker push $IMAGE_NAME:${CI_COMMIT_TAG}; docker push $IMAGE_NAME:${CI_COMMIT_TAG};
fi fi
deploy: deploy:
stage: deploy stage: deploy
image: registry.hottis.de/dockerized/docker-bash:latest image: registry.hottis.de/dockerized/docker-bash:latest
@ -56,7 +54,8 @@ deploy:
- SERVICE_PORT=5000 - SERVICE_PORT=5000
- docker volume inspect $SERVICE_VOLUME || docker volume create $SERVICE_VOLUME - docker volume inspect $SERVICE_VOLUME || docker volume create $SERVICE_VOLUME
- docker stop $CONTAINER_NAME || echo "$CONTAINER_NAME not running, anyway okay" - docker stop $CONTAINER_NAME || echo "$CONTAINER_NAME not running, anyway okay"
- docker rm $CONTAINER_NAME || echo "$CONTAINER_NAME not exsting, anyway okay"
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY; - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY;
- docker pull $IMAGE_NAME:${CI_COMMIT_TAG} - docker pull $IMAGE_NAME:${CI_COMMIT_TAG}
- docker run -d --restart always --name "hv2-api" --network docker-server --ip 172.16.10.38 -v $SERVICE_VOLUME:/opt/app/config $IMAGE_NAME:${CI_COMMIT_TAG} - docker run -d --restart always --name $CONTAINER_NAME --network docker-server --ip 172.16.10.38 -v $SERVICE_VOLUME:/opt/app/config $IMAGE_NAME:${CI_COMMIT_TAG}

View File

@ -6,10 +6,6 @@ LABEL ImageName="registry.hottis.de/hv2/hv2-api"
ARG APP_DIR="/opt/app" ARG APP_DIR="/opt/app"
ARG CONF_DIR="${APP_DIR}/config" ARG CONF_DIR="${APP_DIR}/config"
ENV DB_HOST="172.16.10.18"
ENV DB_NAME="hausverwaltung"
ENV DB_USER="hausverwaltung-ui"
ENV DB_PASS="test123"
RUN \ RUN \
@ -22,7 +18,8 @@ RUN \
pip3 install uwsgi && \ pip3 install uwsgi && \
pip3 install flask-cors && \ pip3 install flask-cors && \
pip3 install python-jose[cryptography] && \ pip3 install python-jose[cryptography] && \
pip3 install loguru pip3 install loguru && \
pip3 install Cheetah3
@ -32,13 +29,19 @@ RUN \
useradd -d ${APP_DIR} -u 1000 user useradd -d ${APP_DIR} -u 1000 user
COPY *.py ${APP_DIR}/ COPY *.py ${APP_DIR}/
COPY openapi.yaml ${APP_DIR}/ COPY openapi.yaml.tmpl ${APP_DIR}/
COPY methods.py.tmpl ${APP_DIR}/
COPY schema.json ${APP_DIR}/
COPY server.ini ${CONF_DIR}/ COPY server.ini ${CONF_DIR}/
USER 1000:1000
WORKDIR ${APP_DIR} WORKDIR ${APP_DIR}
VOLUME ${CONF_DIR} VOLUME ${CONF_DIR}
RUN \
python3 generate.py
USER 1000:1000
EXPOSE 5000 EXPOSE 5000
EXPOSE 9191 EXPOSE 9191

10
ENV.tmp Normal file
View File

@ -0,0 +1,10 @@
# copy to ENV and adjust values
JWT_PUB_KEY="..."
DB_USER="hv2"
DB_PASS="..."
DB_HOST="172.16.10.27"
DB_NAME="hv2"

View File

@ -1,2 +0,0 @@
# copy to ENV and adjust values

12
auth.py
View File

@ -2,7 +2,7 @@ from jose import JWTError, jwt
import werkzeug import werkzeug
import os import os
from loguru import logger from loguru import logger
import json
JWT_PUB_KEY = "" JWT_PUB_KEY = ""
try: try:
@ -19,9 +19,9 @@ def decodeToken(token):
logger.error("{}".format(e)) logger.error("{}".format(e))
raise werkzeug.exceptions.Unauthorized() raise werkzeug.exceptions.Unauthorized()
def testToken(user, token_info):
return '''
You are user_id {user} and the provided token has been signed by this issuers. Fine.'.
Decoded token claims: {token_info}.
'''.format(user=user, token_info=token_info)
def testToken(user, token_info):
return {
"message": f"You are user_id {user} and the provided token has been signed by this issuers. Fine.",
"details": json.dumps(token_info)
}

29
create.sql.tmpl Normal file
View File

@ -0,0 +1,29 @@
#for $table in $tables
CREATE TABLE ${table.name}_t (
id serial not null primary key
#for $column in $table.columns
,$column.name $column.sqltype #slurp
#if (('notnull' in $column) and $column.notnull)
not null #slurp
#end if
#if (('primarykey' in $column) and $column.primarykey)
primary key #slurp
#end if
#if (('foreignkey' in $column) and $column.foreignkey)
references ${column.name}_t (id) #slurp
#end if
#if ('default' in $column)
default $column.default #slurp
#end if
#end for
#if ('tableConstraints' in $table)
#for $tableConstraint in $table.tableConstraints
,$tableConstraint
#end for
#end if
);
#end for

103
db.py Normal file
View File

@ -0,0 +1,103 @@
import psycopg2
import psycopg2.extras
from loguru import logger
import os
import configparser
import json
import werkzeug
DB_USER = ""
DB_PASS = ""
DB_HOST = ""
DB_NAME = ""
try:
DB_USER = os.environ["DB_USER"]
DB_PASS = os.environ["DB_PASS"]
DB_HOST = os.environ["DB_HOST"]
DB_NAME = os.environ["DB_NAME"]
except KeyError:
config = configparser.ConfigParser()
config.read('/opt/app/config/dbconfig.ini')
DB_USER = config["database"]["user"]
DB_PASS = config["database"]["pass"]
DB_HOST = config["database"]["host"]
DB_NAME = config["database"]["name"]
class NoDataFoundException(Exception): pass
class TooMuchDataFoundException(Exception): pass
def databaseOperation(cursor, params):
cursor.execute('SELECT key, value FROM claims_for_user_v where "user" = %s and application = %s',
params)
for claimObj in cursor:
logger.debug("add claim {} -> {}".format(claimObj[0], claimObj[1]))
return []
def execDatabaseOperation(func, params):
conn = None
cur = None
try:
conn = psycopg2.connect(user = DB_USER, password = DB_PASS,
host = DB_HOST, database = DB_NAME,
sslmode = 'require')
conn.autocommit = False
with conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur:
return func(cur, params)
except psycopg2.Error as err:
raise Exception("Error when connecting to database: {}".format(err))
finally:
if conn:
conn.close()
def _opGetMany(cursor, params):
items = []
cursor.execute(params["statement"])
for itemObj in cursor:
logger.debug("item received {}".format(str(itemObj)))
items.append(itemObj)
return items
def dbGetMany(user, token_info, params):
logger.info("params: {}, token: {}".format(params, json.dumps(token_info)))
try:
return execDatabaseOperation(_opGetMany, params)
except Exception as e:
logger.error(f"Exception: {e}")
raise werkzeug.exceptions.InternalServerError
def _opGetOne(cursor, params):
cursor.execute(params["statement"], params["params"])
itemObj = cursor.fetchone()
logger.debug(f"item received: {itemObj}")
if not itemObj:
raise NoDataFoundException
dummyObj = cursor.fetchone()
if dummyObj:
raise TooMuchDataFoundException
return itemObj
def dbGetOne(user, token_info, params):
logger.info("params: {}, token: {}".format(params, json.dumps(token_info)))
try:
return execDatabaseOperation(_opGetOne, params)
except NoDataFoundException:
logger.error("no data found")
raise werkzeug.exceptions.NotFound
except TooMuchDataFoundException:
logger.error("too much data found")
raise werkzeug.exceptions.InternalServerError
except Exception as e:
logger.error(f"Exception: {e}")
raise werkzeug.exceptions.InternalServerError

49
generate.py Normal file
View File

@ -0,0 +1,49 @@
import json
from Cheetah.Template import Template
import glob
import argparse
parser = argparse.ArgumentParser(description="generate.py")
parser.add_argument('--schema', '-s',
help='Schema file. Default: schema.json in the current folder.',
required=False,
default='./schema.json')
parser.add_argument('--template', '-t',
help="""Template file, templates files must be named as the final output file
with an additional .tmpl extension. Default: all template files in the current folder.""",
required=False,
default="*.tmpl")
args = parser.parse_args()
with open(args.schema) as schemaFile:
schema = json.load(schemaFile)
for table in schema["tables"]:
for column in table["columns"]:
if column["sqltype"] == 'serial':
column["apitype"] = 'integer'
column["jstype"] = 'number'
elif column["sqltype"] == 'integer':
column["apitype"] = 'integer'
column["jstype"] = 'number'
elif column["sqltype"] == 'date':
column["apitype"] = 'string'
column["jstype"] = 'string'
elif column["sqltype"] == 'timestamp':
column["apitype"] = 'string'
column["jstype"] = 'string'
elif column["sqltype"].startswith('varchar'):
column["apitype"] = 'string'
column["jstype"] = 'string'
elif column["sqltype"].startswith('numeric'):
column["apitype"] = 'number'
column["jstype"] = 'number'
for f in glob.glob(args.template):
print(f"process {f}")
tmpl = Template(file=f, searchList=[schema])
with open(f[:-5], 'w') as outFile:
outFile.write(str(tmpl))

3
generateHelper.py Normal file
View File

@ -0,0 +1,3 @@
def JsNameConverter(tmplName):
return ''.join([x.capitalize() for x in tmplName.split('_')])

32
methods.py.tmpl Normal file
View File

@ -0,0 +1,32 @@
from db import dbGetMany, dbGetOne
#for $table in $tables
def get_${table.name}s(user, token_info):
return dbGetMany(user, token_info, {
"statement": """
SELECT
id
#for $column in $table.columns
,$column.name
#end for
FROM ${table.name}_t
""",
"params": ()
}
)
def get_${table.name}(user, token_info, ${table.name}Id=None):
return dbGetOne(user, token_info, {
"statement": """
SELECT
id
#for $column in $table.columns
,$column.name
#end for
FROM ${table.name}_t
WHERE id = %s
""",
"params": (${table.name}Id, )
}
)
#end for

View File

@ -1,29 +0,0 @@
openapi: 3.0.0
info:
title: hv2-api
version: "0.1"
paths:
/test:
get:
tags: [ "Test" ]
summary: Return secret string
operationId: auth.testToken
responses:
'200':
description: secret response
content:
'text/plain':
schema:
type: string
security:
- jwt: ['secret']
components:
securitySchemes:
jwt:
type: http
scheme: bearer
bearerFormat: JWT
x-bearerInfoFunc: auth.decodeToken

75
openapi.yaml.tmpl Normal file
View File

@ -0,0 +1,75 @@
openapi: 3.0.0
info:
title: hv2-api
version: "1"
description: "REST-API for the Nober Grundbesitz GbR Hausverwaltungs-Software"
termsOfService: "https://home.hottis.de/dokuwiki/doku.php?id=hv2pub:termsofuse"
contact:
name: "Wolfgang Hottgenroth"
email: "wolfgang.hottgenroth@icloud.com"
externalDocs:
description: "Find more details here"
url: "https://home.hottis.de/dokuwiki/doku.php?id=hv2pub:externaldocs"
paths:
#for $table in $tables
/v1/${table.name}s:
get:
tags: [ "$table.name" ]
summary: Return all normalized ${table.name}s
operationId: methods.get_${table.name}s
responses:
'200':
description: ${table.name}s response
content:
'application/json':
schema:
type: array
items:
\$ref: '#/components/schemas/$table.name'
security:
- jwt: ['secret']
/v1/${table.name}s/{${table.name}Id}:
get:
tags: [ "$table.name" ]
summary: Return the normalized $table.name with given id
operationId: methods.get_$table.name
parameters:
- name: ${table.name}Id
in: path
required: true
schema:
type: integer
responses:
'200':
description: $table.name response
content:
'application/json':
schema:
type: array
items:
\$ref: '#/components/schemas/$table.name'
security:
- jwt: ['secret']
#end for
components:
securitySchemes:
jwt:
type: http
scheme: bearer
bearerFormat: JWT
x-bearerInfoFunc: auth.decodeToken
schemas:
#for $table in $tables
$table.name:
description: $table.name
type: object
properties:
id:
type: integer
#for $column in $table.columns
$column.name:
type: $column.apitype
#end for
#end for

119
schema.json Normal file
View File

@ -0,0 +1,119 @@
{
"tables": [
{
"name": "account",
"columns": [
{ "name": "description", "sqltype": "varchar(128)", "notnull": true }
]
},
{
"name": "tenant",
"columns": [
{ "name": "salutation", "sqltype": "varchar(128)" },
{ "name": "firstname", "sqltype": "varchar(128)" },
{ "name": "lastname", "sqltype": "varchar(128)" },
{ "name": "address1", "sqltype": "varchar(128)" },
{ "name": "address2", "sqltype": "varchar(128)" },
{ "name": "address3", "sqltype": "varchar(128)" },
{ "name": "zip", "sqltype": "varchar(10)" },
{ "name": "city", "sqltype": "varchar(128)" },
{ "name": "phone1", "sqltype": "varchar(64)" },
{ "name": "phone2", "sqltype": "varchar(64)" },
{ "name": "iban", "sqltype": "varchar(64)" },
{ "name": "account", "sqltype": "integer", "notnull": true, "foreignkey": true }
]
},
{
"name": "premise",
"columns": [
{ "name": "description", "sqltype": "varchar(128)" },
{ "name": "street", "sqltype": "varchar(128)", "notnull": true },
{ "name": "zip", "sqltype": "varchar(10)", "notnull": true },
{ "name": "city", "sqltype": "varchar(128)", "notnull": true }
]
},
{
"name": "flat",
"columns": [
{ "name": "description", "sqltype": "varchar(128)" },
{ "name": "premise", "sqltype": "integer", "foreignkey": true },
{ "name": "area", "sqltype": "numeric(10,2)", "notnull": true },
{ "name": "flat_no", "sqltype": "integer" }
]
},
{
"name": "overhead_advance",
"columns": [
{ "name": "description", "sqltype": "varchar(128)" },
{ "name": "amount", "sqltype": "numeric(10,4)", "notnull": true },
{ "name": "startdate", "sqltype": "date" },
{ "name": "enddate", "sqltype": "date" }
]
},
{
"name": "overhead_advance_flat_mapping",
"columns": [
{ "name": "overhead_advance", "sqltype": "integer", "notnull": true, "foreignkey": true },
{ "name": "flat", "sqltype": "integer", "notnull": true, "foreignkey": true }
]
},
{
"name": "parking",
"columns": [
{ "name": "description", "sqltype": "varchar(128)" },
{ "name": "premise", "sqltype": "integer", "foreignkey": true }
]
},
{
"name": "commercial_premise",
"columns": [
{ "name": "description", "sqltype": "varchar(128)" },
{ "name": "premise", "sqltype": "integer", "foreignkey": true }
]
},
{
"name": "tenancy",
"columns": [
{ "name": "description", "sqltype": "varchar(128)" },
{ "name": "tenant", "sqltype": "integer", "notnull": true, "foreignkey": true },
{ "name": "flat", "sqltype": "integer", "notnull": false, "foreignkey": true },
{ "name": "parking", "sqltype": "integer", "notnull": false, "foreignkey": true },
{ "name": "commercial_premise", "sqltype": "integer", "notnull": false, "foreignkey": true },
{ "name": "startdate", "sqltype": "date", "notnull": true },
{ "name": "enddate", "sqltype": "date", "notnull": false }
],
"tableConstraints": [
"constraint tenancy_only_one_object check ((flat is not null and parking is null and commercial_premise is null) or (flat is null and parking is not null and commercial_premise is null) or (flat is null and parking is null and commercial_premise is not null))"
]
},
{
"name": "fee",
"columns": [
{ "name": "description", "sqltype": "varchar(128)" },
{ "name": "amount", "sqltype": "numeric(10,2)", "notnull": true },
{ "name": "fee_type", "sqltype": "varchar(10)", "notnull": true },
{ "name": "startdate", "sqltype": "date" },
{ "name": "enddate", "sqltype": "date" }
],
"tableConstraints": [
"constraint fee_fee_type check (fee_type = 'per_area' or fee_type = 'total')"
]
},
{
"name": "tenancy_fee_mapping",
"columns": [
{ "name": "tenancy", "sqltype": "integer", "notnull": true, "foreignkey": true },
{ "name": "fee", "sqltype": "integer", "notnull": true, "foreignkey": true }
]
},
{
"name": "account_entry",
"columns": [
{ "name": "description", "sqltype": "varchar(128)", "notnull": true },
{ "name": "account", "sqltype": "integer", "notnull": true, "foreignkey": true },
{ "name": "created_at", "sqltype": "timestamp", "notnull": true, "default": "now()" },
{ "name": "amount", "sqltype": "numeric(10,2)", "notnull": true }
]
}
]
}

48
testdata/testdata.sql vendored Normal file
View File

@ -0,0 +1,48 @@
insert into account_t (description) values ('account testtenant1');
insert into tenant_t (lastname, account) values (
'testtenant1',
(select id from account_t where description = 'account testtenant1')
);
insert into premise_t (street, zip, city) values ('Hemsingskotten 38', '45259', 'Essen');
insert into parking_t (description, premise) values ('Garage 1', (select id from premise_t where street = 'Hemsingskotten 38'));
insert into flat_t (description, premise, area, flat_no) values('EG links', (select id from premise_t where street = 'Hemsingskotten 38'), 59.0, 1);
insert into fee_t (description, amount, fee_type, startdate) values ('Altenwohnung Hemsingskotten', 5.23, 'per_area', '2021-01-01');
insert into fee_t (description, amount, fee_type, startdate) values ('Garage intern', 30.0, 'total', '2021-01-01');
insert into tenancy_t (tenant, flat, description, startdate) values (
(select id from tenant_t where lastname = 'testtenant1'),
(select id from flat_t where flat_no = 1),
'flat testtenant1',
'2021-06-01'
);
insert into tenancy_t (tenant, parking, description, startdate) values (
(select id from tenant_t where lastname = 'testtenant1'),
(select id from parking_t where description = 'Garage 1'),
'parking testtenant1',
'2021-06-01'
);
insert into overhead_advance_t (description, amount, startdate) values ('BKV Altenwohnung Hemsingskotten', 0.34, '2021-01-01');
insert into overhead_advance_flat_mapping_t (overhead_advance, flat) values (
(select id from overhead_advance_t where description = 'BKV Altenwohnung Hemsingskotten'),
(select id from flat_t where flat_no = 1)
);
insert into tenancy_fee_mapping_t (tenancy, fee) values (
(select id from tenancy_t where description = 'flat testtenant1'),
(select id from fee_t where description = 'Altenwohnung Hemsingskotten')
);
insert into tenancy_fee_mapping_t (tenancy, fee) values (
(select id from tenancy_t where description = 'parking testtenant1'),
(select id from fee_t where description = 'Garage intern')
);