bugfixes, cleanup
2
.gitignore
vendored
|
@ -1,6 +1,6 @@
|
||||||
/venv
|
/venv
|
||||||
/tmp
|
/tmp
|
||||||
/production.cfg
|
/lenticular_cloud/production.cfg
|
||||||
/pki
|
/pki
|
||||||
signing_key.pem
|
signing_key.pem
|
||||||
node_modules
|
node_modules
|
||||||
|
|
4
MANIFEST.in
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
recursive-include lenticular_cloud/template *
|
||||||
|
recursive-include lenticular_cloud/static **
|
||||||
|
include lenticular_cloud/*.cfg
|
||||||
|
|
|
@ -32,3 +32,9 @@ Tested Services
|
||||||
* ~~Postfix~~/Dovecot (~~OIDC~~/client cert/password)
|
* ~~Postfix~~/Dovecot (~~OIDC~~/client cert/password)
|
||||||
* prosody (client cert/~~password~~)
|
* prosody (client cert/~~password~~)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Oauth2 Settings:
|
||||||
|
----------------
|
||||||
|
|
||||||
|
callback url: `${domain}/
|
||||||
|
|
8
cli.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from lenticular_cloud.cli import entry_point
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
entry_point()
|
13
hydra_client.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"client_id": "identiy_provider",
|
||||||
|
"client_secret": "",
|
||||||
|
"redirect_uris": [
|
||||||
|
"{domain}/oauth/authorized"
|
||||||
|
],
|
||||||
|
"response_types": [
|
||||||
|
"code", "id_token"
|
||||||
|
],
|
||||||
|
"scope": "openid profile manage",
|
||||||
|
"grant_types": ["authorization_code", "refresh_token"],
|
||||||
|
"token_endpoint_auth_method": "client_secret_basic"
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import time
|
||||||
import subprocess
|
import subprocess
|
||||||
import ory_hydra_client as hydra
|
import ory_hydra_client as hydra
|
||||||
import ory_hydra_client.api.admin_api as hydra_admin_api
|
import ory_hydra_client.api.admin_api as hydra_admin_api
|
||||||
|
import os
|
||||||
|
|
||||||
from ldap3 import Connection, Server, ALL
|
from ldap3 import Connection, Server, ALL
|
||||||
|
|
||||||
|
@ -26,11 +27,12 @@ def init_oauth2(app):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def init_app(name=None):
|
def create_app():
|
||||||
name = name or __name__
|
name = "lenticular_cloud"
|
||||||
app = Flask(name)
|
app = Flask(name, template_folder='template')
|
||||||
app.config.from_pyfile('application.cfg')
|
app.config.from_pyfile('application.cfg')
|
||||||
app.config.from_pyfile('production.cfg')
|
active_cfg = os.getenv('CONFIG_FILE', 'production.cfg')
|
||||||
|
app.config.from_pyfile(active_cfg)
|
||||||
|
|
||||||
app.jinja_env.globals['GIT_HASH'] = get_git_hash()
|
app.jinja_env.globals['GIT_HASH'] = get_git_hash()
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ SESSION_COOKIE_NAME='lc_session'
|
||||||
SUBJECT_ID_HASH_SALT = 'salt'
|
SUBJECT_ID_HASH_SALT = 'salt'
|
||||||
PREFERRED_URL_SCHEME = 'https'
|
PREFERRED_URL_SCHEME = 'https'
|
||||||
|
|
||||||
DATA_FOLDER = "./data"
|
DATA_FOLDER = "../data"
|
||||||
|
|
||||||
SQLALCHEMY_DATABASE_URI = f'sqlite:///{DATA_FOLDER}/db.sqlite'
|
SQLALCHEMY_DATABASE_URI = f'sqlite:///{DATA_FOLDER}/db.sqlite'
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS=False
|
SQLALCHEMY_TRACK_MODIFICATIONS=False
|
|
@ -70,7 +70,7 @@ class TotpAuthProvider(AuthProvider):
|
||||||
def check_auth(user, form):
|
def check_auth(user, form):
|
||||||
data = form.data['totp']
|
data = form.data['totp']
|
||||||
if data is not None:
|
if data is not None:
|
||||||
print(f'data totp: {data}')
|
#print(f'data totp: {data}')
|
||||||
if len(user.totps) == 0: # migration, TODO remove
|
if len(user.totps) == 0: # migration, TODO remove
|
||||||
return True
|
return True
|
||||||
for totp in user.totps:
|
for totp in user.totps:
|
||||||
|
@ -84,5 +84,5 @@ AUTH_PROVIDER_LIST = [
|
||||||
TotpAuthProvider
|
TotpAuthProvider
|
||||||
]
|
]
|
||||||
|
|
||||||
print(LdapAuthProvider.get_name())
|
#print(LdapAuthProvider.get_name())
|
||||||
|
|
||||||
|
|
61
lenticular_cloud/cli.py
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import argparse
|
||||||
|
from .model import db, User, UserSignUp
|
||||||
|
from .app import create_app
|
||||||
|
|
||||||
|
|
||||||
|
def entry_point():
|
||||||
|
parser = argparse.ArgumentParser(description='lenticular_cloud cli')
|
||||||
|
subparsers = parser.add_subparsers()
|
||||||
|
|
||||||
|
parser_user = subparsers.add_parser('user')
|
||||||
|
parser_user.set_defaults(func=cli_user)
|
||||||
|
|
||||||
|
parser_signup = subparsers.add_parser('signup')
|
||||||
|
parser_signup.add_argument('--signup_id', type=int)
|
||||||
|
parser_signup.set_defaults(func=cli_signup)
|
||||||
|
|
||||||
|
'''
|
||||||
|
parser_upcoming = subparsers.add_parser('upcoming')
|
||||||
|
parser_upcoming.set_defaults(func=cli_upcoming)
|
||||||
|
parser_upcoming.add_argument('-a', '--all', help='shows all single order`', nargs='?', default=False, const=True,
|
||||||
|
type=bool,
|
||||||
|
required=False)
|
||||||
|
parser_upcoming.add_argument('-n', '--no-import', dest='noimport', help='do not a automatic import', nargs='?',
|
||||||
|
default=False, type=bool, required=False)
|
||||||
|
parser_upcoming.add_argument('-F', '--format', help='format can be `d`|`m`|`y`', default='d', required=False)
|
||||||
|
|
||||||
|
# parser_select.add_argument('-F', '--format', help='format can be `d`|`m`|`y`', default='d', required=False)
|
||||||
|
# parser_select.add_argument('-f', '--from', help='from date in the format `yyyy-mm-dd`', required=False)
|
||||||
|
# parser_select.add_argument('-t', '--to', help='to date in the format `yyyy-mm-dd`', required=False)
|
||||||
|
'''
|
||||||
|
args = parser.parse_args()
|
||||||
|
if 'func' not in args:
|
||||||
|
parser.print_help()
|
||||||
|
return
|
||||||
|
app = create_app()
|
||||||
|
with app.app_context():
|
||||||
|
args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
def cli_user(args):
|
||||||
|
print(User.query.all())
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cli_signup(args):
|
||||||
|
|
||||||
|
print(args.signup_id)
|
||||||
|
if args.signup_id is not None:
|
||||||
|
user_data = UserSignUp.query.get(args.signup_id)
|
||||||
|
user = User.new(user_data)
|
||||||
|
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.delete(user_data)
|
||||||
|
db.session.commit()
|
||||||
|
else:
|
||||||
|
# list
|
||||||
|
print(UserSignUp.query.all())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
entry_point()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from ldap3_orm import AttrDef, EntryBase as _EntryBase, ObjectDef, EntryType
|
from ldap3_orm import AttrDef, EntryBase as _EntryBase, ObjectDef, EntryType
|
||||||
from ldap3_orm import Reader
|
from ldap3_orm import Reader
|
||||||
from ldap3 import Entry, HASHED_SALTED_SHA256
|
from ldap3 import Connection, Entry, HASHED_SALTED_SHA256
|
||||||
from ldap3.utils.conv import escape_filter_chars
|
from ldap3.utils.conv import escape_filter_chars
|
||||||
from ldap3.utils.hashed import hashed
|
from ldap3.utils.hashed import hashed
|
||||||
from flask_login import UserMixin
|
from flask_login import UserMixin
|
||||||
|
@ -19,6 +19,7 @@ from flask_sqlalchemy import SQLAlchemy, orm
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import uuid
|
import uuid
|
||||||
import pyotp
|
import pyotp
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -226,6 +227,8 @@ class User(EntryBase):
|
||||||
db.String(length=36), primary_key=True, default=generate_uuid)
|
db.String(length=36), primary_key=True, default=generate_uuid)
|
||||||
username = db.Column(
|
username = db.Column(
|
||||||
db.String, unique=True, nullable=False)
|
db.String, unique=True, nullable=False)
|
||||||
|
alternative_email = db.Column(
|
||||||
|
db.String, nullable=True)
|
||||||
created_at = db.Column(db.DateTime, nullable=False,
|
created_at = db.Column(db.DateTime, nullable=False,
|
||||||
default=datetime.now)
|
default=datetime.now)
|
||||||
modified_at = db.Column(db.DateTime, nullable=False,
|
modified_at = db.Column(db.DateTime, nullable=False,
|
||||||
|
@ -236,7 +239,7 @@ class User(EntryBase):
|
||||||
|
|
||||||
dn = "uid={uid},{base_dn}"
|
dn = "uid={uid},{base_dn}"
|
||||||
base_dn = "ou=users,{_base_dn}"
|
base_dn = "ou=users,{_base_dn}"
|
||||||
object_classes = ["top", "inetOrgPerson", "LenticularUser"]
|
object_classes = ["inetOrgPerson"] #, "LenticularUser"]
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self._ldap_object = None
|
self._ldap_object = None
|
||||||
|
@ -247,57 +250,34 @@ class User(EntryBase):
|
||||||
return True # TODO
|
return True # TODO
|
||||||
|
|
||||||
def get(self, key):
|
def get(self, key):
|
||||||
print(f'getitem: {key}')
|
print(f'getitem: {key}') # TODO
|
||||||
|
|
||||||
def make_writeable(self):
|
def make_writeable(self):
|
||||||
self._ldap_object = self._ldap_object.entry_writable()
|
self._ldap_object = self._ldap_object.entry_writable()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def groups(self):
|
def groups(self) -> list[str]:
|
||||||
if self.username == 'tuxcoder':
|
if self.username == 'tuxcoder':
|
||||||
return [Group(name='admin')]
|
return [Group(name='admin')]
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def entry_dn(self):
|
def entry_dn(self) -> str:
|
||||||
return self._ldap_object.entry_dn
|
return self._ldap_object.entry_dn
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fullname(self):
|
def email(self) -> str:
|
||||||
return self._ldap_object.fullname
|
|
||||||
|
|
||||||
@property
|
|
||||||
def givenname(self):
|
|
||||||
return self._ldap_object.givenname
|
|
||||||
|
|
||||||
@property
|
|
||||||
def surname(self):
|
|
||||||
return self._ldap_object.surname
|
|
||||||
|
|
||||||
@property
|
|
||||||
def email(self):
|
|
||||||
domain = current_app.config['DOMAIN']
|
domain = current_app.config['DOMAIN']
|
||||||
return f'{self.username}@{domain}'
|
return f'{self.username}@{domain}'
|
||||||
return self._ldap_object.mail
|
return self._ldap_object.mail
|
||||||
|
|
||||||
@property
|
def change_password(self, password_new: str) -> bool:
|
||||||
def alternative_email(self):
|
|
||||||
return self._ldap_object.altMail
|
|
||||||
|
|
||||||
@property
|
|
||||||
def auth_role(self):
|
|
||||||
return self._ldap_object.authRole
|
|
||||||
|
|
||||||
@property
|
|
||||||
def gpg_public_key(self):
|
|
||||||
return self._ldap_object.gpgPublicKey
|
|
||||||
|
|
||||||
def change_password(self, password_new: str):
|
|
||||||
self.make_writeable()
|
self.make_writeable()
|
||||||
password_hashed = crypt.crypt(password_new)
|
password_hashed = crypt.crypt(password_new)
|
||||||
self._ldap_object.userPassword = ('{CRYPT}' + password_hashed).encode()
|
self._ldap_object.userPassword = ('{CRYPT}' + password_hashed).encode()
|
||||||
self.ldap_commit()
|
self.ldap_commit()
|
||||||
|
return True
|
||||||
|
|
||||||
class _query(EntryBase._query):
|
class _query(EntryBase._query):
|
||||||
|
|
||||||
|
@ -312,7 +292,7 @@ class User(EntryBase):
|
||||||
user._ldap_object = ldap_object
|
user._ldap_object = ldap_object
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def by_username(self, username) -> 'User':
|
def by_username(self, username) -> Optional['User']:
|
||||||
result = self._query('(uid={username:s})'.format(username=escape_filter_chars(username)))
|
result = self._query('(uid={username:s})'.format(username=escape_filter_chars(username)))
|
||||||
if len(result) > 0:
|
if len(result) > 0:
|
||||||
return result[0]
|
return result[0]
|
||||||
|
|
Before Width: | Height: | Size: 876 KiB After Width: | Height: | Size: 876 KiB |
Before Width: | Height: | Size: 730 KiB After Width: | Height: | Size: 730 KiB |
Before Width: | Height: | Size: 699 KiB After Width: | Height: | Size: 699 KiB |
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 141 KiB |
Before Width: | Height: | Size: 898 KiB After Width: | Height: | Size: 898 KiB |
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 141 KiB |
|
@ -1,6 +1,7 @@
|
||||||
from flask import g, request, Flask
|
from flask import g, request, Flask
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from lenticular_cloud.model import db, User
|
||||||
|
|
||||||
LANGUAGES = {
|
LANGUAGES = {
|
||||||
'en': 'English',
|
'en': 'English',
|
||||||
|
|
|
@ -15,9 +15,9 @@ def before_request():
|
||||||
try:
|
try:
|
||||||
resp = current_app.oauth.session.get('/userinfo')
|
resp = current_app.oauth.session.get('/userinfo')
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
if not current_user.is_authenticated or resp.status_code is not 200:
|
if not current_user.is_authenticated or resp.status_code != 200:
|
||||||
return redirect_login()
|
return redirect_login()
|
||||||
if 'admin' not in data['groups']:
|
if 'groups' not in data or 'admin' not in data['groups']:
|
||||||
return 'Not an admin', 403
|
return 'Not an admin', 403
|
||||||
except TokenExpiredError:
|
except TokenExpiredError:
|
||||||
return redirect_login()
|
return redirect_login()
|
||||||
|
|
|
@ -86,7 +86,8 @@ def login():
|
||||||
login_challenge = request.args.get('login_challenge')
|
login_challenge = request.args.get('login_challenge')
|
||||||
try:
|
try:
|
||||||
login_request = current_app.hydra_api.get_login_request(login_challenge)
|
login_request = current_app.hydra_api.get_login_request(login_challenge)
|
||||||
except ory_hydra_client.exceptions.ApiException:
|
except ory_hydra_client.exceptions.ApiException as e:
|
||||||
|
logger.exception("could not fetch login request")
|
||||||
return redirect(url_for('frontend.index'))
|
return redirect(url_for('frontend.index'))
|
||||||
|
|
||||||
if login_request.skip:
|
if login_request.skip:
|
||||||
|
|
|
@ -31,7 +31,7 @@ def redirect_login():
|
||||||
def before_request():
|
def before_request():
|
||||||
try:
|
try:
|
||||||
resp = current_app.oauth.session.get('/userinfo')
|
resp = current_app.oauth.session.get('/userinfo')
|
||||||
if not current_user.is_authenticated or resp.status_code is not 200:
|
if not current_user.is_authenticated or resp.status_code != 200:
|
||||||
logger.info('user not logged in redirect')
|
logger.info('user not logged in redirect')
|
||||||
return redirect_login()
|
return redirect_login()
|
||||||
except TokenExpiredError:
|
except TokenExpiredError:
|
||||||
|
@ -72,7 +72,7 @@ def init_login_manager(app):
|
||||||
if not token:
|
if not token:
|
||||||
flash("Failed to log in.", category="error")
|
flash("Failed to log in.", category="error")
|
||||||
return False
|
return False
|
||||||
print(f'debug ---------------{token}')
|
#print(f'debug ---------------{token}')
|
||||||
|
|
||||||
resp = blueprint.session.get("/userinfo")
|
resp = blueprint.session.get("/userinfo")
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
|
|
4
lenticular_cloud/wsgi.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
from .app import create_app
|
||||||
|
|
||||||
|
application = create_app()
|
||||||
|
|
1
requirements-dev.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
flask-debug
|
|
@ -17,5 +17,4 @@ requests_oauthlib
|
||||||
blinker
|
blinker
|
||||||
ory-hydra-client
|
ory-hydra-client
|
||||||
|
|
||||||
flask-debug
|
|
||||||
|
|
||||||
|
|
60
schema/lenticular.ldif
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
dn: cn=lenticular,cn=schema,cn=config
|
||||||
|
objectClass: olcSchemaConfig
|
||||||
|
cn: lenticular
|
||||||
|
olcAttributeTypes: ( 1.3.6.1.4.1.18060.0.4.3.2.1
|
||||||
|
NAME 'masterPasswordEnable'
|
||||||
|
DESC 'is the master password enabled'
|
||||||
|
EQUALITY booleanMatch
|
||||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
|
||||||
|
SINGLE-VALUE )
|
||||||
|
olcAttributeTypes: ( 1.3.6.1.4.1.18060.0.4.3.2.2
|
||||||
|
NAME 'authRole'
|
||||||
|
DESC 'is the master password enabled'
|
||||||
|
EQUALITY caseIgnoreMatch
|
||||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
|
||||||
|
olcAttributeTypes: ( 1.3.6.1.4.1.18060.0.4.3.2.3
|
||||||
|
NAME ( 'altMail' )
|
||||||
|
DESC 'RFC1274: RFC822 Mailbox'
|
||||||
|
EQUALITY caseIgnoreIA5Match
|
||||||
|
SUBSTR caseIgnoreIA5SubstringsMatch
|
||||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )
|
||||||
|
olcAttributeTypes: ( 1.3.6.1.4.1.18060.0.4.3.2.4
|
||||||
|
NAME 'gpgPublicKey'
|
||||||
|
DESC 'pgpPublicKey as ascii text'
|
||||||
|
EQUALITY octetStringMatch
|
||||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
|
||||||
|
olcAttributeTypes: ( 1.3.6.1.4.1.18060.0.4.3.2.5
|
||||||
|
NAME 'totpSecret'
|
||||||
|
DESC 'TOTP secret as base32'
|
||||||
|
EQUALITY octetStringMatch
|
||||||
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
|
||||||
|
olcObjectClasses: ( 1.3.6.1.4.1.18060.0.4.3.3.1
|
||||||
|
NAME 'service'
|
||||||
|
DESC 'schema for a service'
|
||||||
|
SUP top STRUCTURAL
|
||||||
|
MUST uid
|
||||||
|
MAY ( masterPasswordEnable $ mail ) )
|
||||||
|
olcObjectClasses: ( 1.3.6.1.4.1.18060.0.4.3.3.2
|
||||||
|
NAME 'LenticularUser'
|
||||||
|
DESC 'a Lenticular user'
|
||||||
|
SUP top AUXILIARY
|
||||||
|
MUST uid
|
||||||
|
MAY ( authRole $ altMail $ gpgPublicKey ) )
|
||||||
|
olcObjectClasses: ( 1.3.6.1.4.1.18060.0.4.3.3.3
|
||||||
|
NAME 'LenticularGroup'
|
||||||
|
DESC 'a Lenticular group'
|
||||||
|
SUP top AUXILIARY
|
||||||
|
MUST cn
|
||||||
|
MAY ( authRole ) )
|
||||||
|
#olcObjectClasses: ( 1.3.6.1.4.1.18060.0.4.3.3.4
|
||||||
|
# NAME 'posixAccountAux'
|
||||||
|
# DESC 'Abstraction of an account with POSIX attributes'
|
||||||
|
# SUP top AUXILIARY
|
||||||
|
# MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory )
|
||||||
|
# MAY ( userPassword $ loginShell $ gecos $ description ) )
|
||||||
|
#olcObjectClasses: ( 1.3.6.1.1.1.2.2 NAME 'posixGroup' SUP top AUXILIARY
|
||||||
|
# DESC 'Abstraction of a group of accounts'
|
||||||
|
# MUST gidNumber
|
||||||
|
# MAY ( userPassword $ memberUid $
|
||||||
|
# description ) )
|
||||||
|
|
25
setup.cfg
|
@ -1,25 +0,0 @@
|
||||||
[metadata]
|
|
||||||
name = lenticular_cloud
|
|
||||||
version = 0.0.1
|
|
||||||
description = user management portal
|
|
||||||
|
|
||||||
[options]
|
|
||||||
packages = lenticular_cloud
|
|
||||||
install_requires =
|
|
||||||
Flask
|
|
||||||
gunicorn
|
|
||||||
flask_babel
|
|
||||||
flask_wtf
|
|
||||||
flask_login
|
|
||||||
flask_sqlalchemy
|
|
||||||
Flask-Dance
|
|
||||||
ldap3
|
|
||||||
ldap3_orm
|
|
||||||
python-u2flib-server
|
|
||||||
pyotp
|
|
||||||
cryptography
|
|
||||||
requests
|
|
||||||
requests_oauthlib
|
|
||||||
blinker
|
|
||||||
ory-hydra-client
|
|
||||||
flask-debug
|
|
26
setup.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
TESTS_DIRECTORY = 'test'
|
||||||
|
|
||||||
|
def get_requirements():
|
||||||
|
return []
|
||||||
|
with Path("./requirements.txt").open('r') as fd:
|
||||||
|
return fd.readlines()
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='lenticular_cloud',
|
||||||
|
version='0.0.1',
|
||||||
|
description='A useful module',
|
||||||
|
author='tuxcoder',
|
||||||
|
author_email='git@o-g.at',
|
||||||
|
packages=find_packages(exclude=(TESTS_DIRECTORY,)),
|
||||||
|
include_package_data=True,
|
||||||
|
install_requires=get_requirements(), # external packages as dependencies
|
||||||
|
entry_points = {
|
||||||
|
'console_scripts': ['lenticular_cloud-cli=lenticular_cloud.cli:entry_point']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
4
tox.ini
|
@ -1,5 +1,5 @@
|
||||||
[tox]
|
#[tox]
|
||||||
isolated_build = true
|
#isolated_build = true
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
deps =
|
deps =
|
||||||
|
|