Commit 2daa1056 by aagusti

perbaikan report

1 parent c453d871
...@@ -26,7 +26,7 @@ import datetime, decimal ...@@ -26,7 +26,7 @@ import datetime, decimal
from sqlalchemy import engine_from_config, or_ from sqlalchemy import engine_from_config, or_
from .security import ( from .security import (
group_finder, group_finder,
get_user, get_user, MySecurityPolicy,
) )
from .models import ( from .models import (
DBSession, DBSession,
...@@ -392,15 +392,14 @@ def main(global_config, **settings): ...@@ -392,15 +392,14 @@ def main(global_config, **settings):
# config.include('pyramid_beaker') # config.include('pyramid_beaker')
# config.include('pyramid_chameleon') # config.include('pyramid_chameleon')
authn_policy = AuthTktAuthenticationPolicy( # authn_policy = AuthTktAuthenticationPolicy(
'sosecret', callback=group_finder, hashalg='sha512') # 'sosecret', callback=group_finder, hashalg='sha512')
#
authz_policy = ACLAuthorizationPolicy() # authz_policy = ACLAuthorizationPolicy()
config.set_security_policy(MySecurityPolicy(settings["session.secret"]))
config.set_authentication_policy(authn_policy) # config.set_authentication_policy(authn_policy)
# config.set_security_policy(authz_policy) # config.set_security_policy(authz_policy)
config.set_authorization_policy(authz_policy) # config.set_authorization_policy(authz_policy)
config.add_request_method(get_user, 'user', reify=True) config.add_request_method(get_user, 'user', reify=True)
config.add_request_method(get_title, 'title', reify=True) config.add_request_method(get_title, 'title', reify=True)
config.add_request_method(get_company, 'company', reify=True) config.add_request_method(get_company, 'company', reify=True)
...@@ -426,6 +425,7 @@ def main(global_config, **settings): ...@@ -426,6 +425,7 @@ def main(global_config, **settings):
# config.add_notfound_view(RemoveSlashNotFoundViewFactory()) # config.add_notfound_view(RemoveSlashNotFoundViewFactory())
config.add_static_view('static', 'opensipkd.base:static', cache_max_age=3600) config.add_static_view('static', 'opensipkd.base:static', cache_max_age=3600)
config.add_static_view('deform_static', 'deform:static') config.add_static_view('deform_static', 'deform:static')
# config.add_view('.views.api.echoGateway')
# config.add_static_view('files', get_params('static_files')) # config.add_static_view('files', get_params('static_files'))
# Captcha # Captcha
......
...@@ -188,7 +188,6 @@ class RootFactory: ...@@ -188,7 +188,6 @@ class RootFactory:
acl_name = 'group:{}'.format(gp.group_id) acl_name = 'group:{}'.format(gp.group_id)
self.__acl__.append((Allow, acl_name, gp.perm_name)) self.__acl__.append((Allow, acl_name, gp.perm_name))
def init_model(): def init_model():
ziggurat_model_init(User, Group, UserGroup, GroupPermission, UserPermission, ziggurat_model_init(User, Group, UserGroup, GroupPermission, UserPermission,
UserResourcePermission, GroupResourcePermission, Resource, UserResourcePermission, GroupResourcePermission, Resource,
......
...@@ -40,3 +40,54 @@ def get_user(request): ...@@ -40,3 +40,54 @@ def get_user(request):
# if user_id is not None: # if user_id is not None:
# user = DBSession.query(User).get(user_id) # user = DBSession.query(User).get(user_id)
# return user # return user
from pyramid.authentication import AuthTktCookieHelper
from pyramid.authorization import ACLHelper, Authenticated, Everyone
class MySecurityPolicy:
def __init__(self, secret):
self.helper = AuthTktCookieHelper(secret)
def identity(self, request):
# define our simple identity as None or a dict with userid and principals keys
identity = self.helper.identify(request)
if identity is None:
return None
userid = identity['userid'] # identical to the deprecated request.unauthenticated_userid
# verify the userid, just like we did before with groupfinder
principals = group_finder(userid, request)
# assuming the userid is valid, return a map with userid and principals
if principals is not None:
return {
'userid': userid,
'principals': principals,
}
def authenticated_userid(self, request):
# defer to the identity logic to determine if the user id logged in
# and return None if they are not
identity = request.identity
if identity is not None:
return identity['userid']
def permits(self, request, context, permission):
# use the identity to build a list of principals, and pass them
# to the ACLHelper to determine allowed/denied
identity = request.identity
principals = set([Everyone])
if identity is not None:
principals.add(Authenticated)
principals.add(identity['userid'])
principals.update(identity['principals'])
return ACLHelper().permits(context, principals, permission)
def remember(self, request, userid, **kw):
return self.helper.remember(request, userid, **kw)
def forget(self, request, **kw):
return self.helper.forget(request, **kw)
...@@ -31,6 +31,7 @@ def auth_from(request, field=None): ...@@ -31,6 +31,7 @@ def auth_from(request, field=None):
# bypass cek authentication for development # bypass cek authentication for development
if http_userid == 'admin' and request.devel: if http_userid == 'admin' and request.devel:
return user return user
time_stamp = validate_time(request) time_stamp = validate_time(request)
if field: if field:
header = json_rpc_header(http_userid, user.security_code, time_stamp) header = json_rpc_header(http_userid, user.security_code, time_stamp)
......
# from pyramid_rpc.amfgateway import PyramidGateway
from pyramid_rpc.jsonrpc import jsonrpc_method
from opensipkd.tools.api import JsonRpcInvalidDataError, JsonRpcInvalidLoginError
from ziggurat_foundations.models.services.user import UserService
from ..models import Partner, User
def get_profile_(user):
partner = Partner.query().filter_by(email=user.email).first()
if not partner:
raise JsonRpcInvalidDataError
result = dict(user_name=user.user_name,
nik=partner.kode,
email=partner.email,
mobile=partner.mobile,
nama=partner.nama, )
return dict(data=result)
@jsonrpc_method(method='get_profile', endpoint='rpc-user')
def get_profile(request, data):
"""
Digunakan untuk memperoleh profile user yang sedang login
parameter
@param request: Request
@param data: Dict(user_name=user_name/email, password=password)
@return:
"""
user = request.user
print("User", user)
data = type(data) == list and data[0] or data
user = User.get_by_identity(data["user_name"])
if not user or not UserService.check_password(user, data['password']):
raise JsonRpcInvalidLoginError
return get_profile_(user)
# services = {
# 'get_profile.echo': get_profile
# could include other functions as well
# }
#
# echoGateway = PyramidGateway(services)
...@@ -87,7 +87,7 @@ class Views(BaseView): ...@@ -87,7 +87,7 @@ class Views(BaseView):
return r return r
@view_config( @view_config(
route_name='group-add', renderer='templates/group/add.pt', route_name='group-add', renderer='templates/form.pt',
permission='user-edit') permission='user-edit')
def view_add(self): def view_add(self):
return super(Views, self).view_add() return super(Views, self).view_add()
...@@ -122,13 +122,13 @@ class Views(BaseView): ...@@ -122,13 +122,13 @@ class Views(BaseView):
return values return values
@view_config( @view_config(
route_name='group-view', renderer='templates/group/edit.pt', route_name='group-view', renderer='templates/form.pt',
permission='user-view') permission='user-view')
def view_view(self): def view_view(self):
return super(Views, self).view_view() return super(Views, self).view_view()
@view_config( @view_config(
route_name='group-edit', renderer='templates/group/edit.pt', route_name='group-edit', renderer='templates/form.pt',
permission='user-edit') permission='user-edit')
def view_edit(self): def view_edit(self):
return super(Views, self).view_edit() return super(Views, self).view_edit()
...@@ -142,7 +142,7 @@ class Views(BaseView): ...@@ -142,7 +142,7 @@ class Views(BaseView):
self.ses.flash(ts) self.ses.flash(ts)
@view_config( @view_config(
route_name='group-delete', renderer='templates/group/delete.pt', route_name='group-delete', renderer='templates/form.pt',
permission='user-edit') permission='user-edit')
def view_delete(self): def view_delete(self):
return super(Views, self).view_delete() return super(Views, self).view_delete()
......
import colander import colander
import os
from deform import (widget, ) from deform import (widget, )
from opensipkd.tools.buttons import btn_view, btn_add, btn_edit, btn_delete, btn_close
from opensipkd.tools.report import csv_response, open_rml_pdf, open_rml_row, pdf_response
from pyramid.view import (view_config, ) from pyramid.view import (view_config, )
from .partner_base import NamaSchema from .partner_base import NamaSchema
from ..models import ( from ..models import (
DBSession, DBSession,
Jabatan, Jabatan,
Eselon Eselon, Departemen
) )
from ..views import BaseView, deferred_jenis from ..views import BaseView, deferred_jenis
...@@ -79,6 +82,12 @@ class ViewJabatan(BaseView): ...@@ -79,6 +82,12 @@ class ViewJabatan(BaseView):
self.edit_schema = EditSchema self.edit_schema = EditSchema
self.table = Jabatan self.table = Jabatan
self.list_schema = ListSchema self.list_schema = ListSchema
# self.list_buttons = (btn_view, btn_add, btn_edit, btn_delete, btn_close)
def get_bindings(self, row=None):
return dict(daftar_jenis=JENIS,
daftar_eselon=daftar_eselon())
@view_config(route_name='jabatan', renderer='templates/table.pt', @view_config(route_name='jabatan', renderer='templates/table.pt',
permission='jabatan') permission='jabatan')
...@@ -151,13 +160,42 @@ class ViewJabatan(BaseView): ...@@ -151,13 +160,42 @@ class ViewJabatan(BaseView):
else: else:
nama_jenis = 'Fungsional' nama_jenis = 'Fungsional'
d = {} d = {'id': k[0], 'value': k[2] + ' (' + nama_jenis + ')', 'kode': k[1], 'nama': k[2]}
d['id'] = k[0]
d['value'] = k[2] + ' (' + nama_jenis + ')'
d['kode'] = k[1]
d['nama'] = k[2]
r.append(d) r.append(d)
return r return r
elif url_dict['act'] == 'csv':
query = query_reg(request)
row = query.first()
header = row.keys()
rows = []
for item in query.all():
rows.append(list(item))
filename = 'jabatan.csv'
value = {
'header': header,
'rows': rows,
}
return csv_response(request, value, filename)
elif url_dict['act'] == 'pdf':
query = query_reg(request)
_here = os.path.dirname(__file__) # get current folder -> views
path = os.path.dirname(_here) # mundur 1 level
path = os.path.join(path, 'reports')
rml_row = open_rml_row(path + '/jabatan.row.rml')
rows = []
for r in query.all():
s = rml_row.format(kode=r.kode, nama=r.nama, status=r.status and "Aktif" or "Pasif")
rows.append(s)
pdf, filename = open_rml_pdf(path + '/jabatan.rml', rows=rows,
company=request.company,
departement=request.session['departemen_nm'],
address=request.address,
alamat=Departemen.query_id(request.session['departemen_id']).first(),
periode='01-01-2017 s.d 31-12-2017')
return pdf_response(request, pdf, filename)
@view_config(route_name='jabatan-add', @view_config(route_name='jabatan-add',
renderer='templates/form.pt', renderer='templates/form.pt',
...@@ -213,3 +251,11 @@ class ViewJabatan(BaseView): ...@@ -213,3 +251,11 @@ class ViewJabatan(BaseView):
err_nama() err_nama()
elif found: elif found:
err_nama() err_nama()
def query_reg(request):
return DBSession.query(Jabatan.kode,
Jabatan.nama,
Jabatan.status, ). \
filter(Jabatan.status == 1). \
order_by(Jabatan.id)
import os import os
import uuid
# from ..tools import row2dict, xls_reader
from email.utils import parseaddr
from datetime import datetime from datetime import datetime
from sqlalchemy import not_, func, or_, desc
from sqlalchemy.orm import aliased
from pyramid.view import (
view_config,
)
from pyramid.httpexceptions import (HTTPFound, )
import json
import colander
from deform import (
Form,
widget,
ValidationFailure,
)
from ..views import (ColumnDT, DataTables, BaseView, )
from opensipkd.tools.report import ( from opensipkd.tools.report import (
open_rml_row, open_rml_row,
open_rml_pdf, open_rml_pdf,
pdf_response, pdf_response,
csv_response, csv_response,
get_logo, ) )
from opensipkd.tools import (dmy, date_from_str, thousand, STATUS) from pyramid.view import (
view_config,
)
from ..models import DBSession from ..models import DBSession
from ..models import ( from ..models import (
Jabatan, Jabatan,
Departemen, Departemen,
) )
from ..views import (BaseView, )
class view_rpt(BaseView): class view_rpt(BaseView):
......
...@@ -3,8 +3,8 @@ import re ...@@ -3,8 +3,8 @@ import re
import colander import colander
from deform import (widget, ) from deform import (widget, )
from opensipkd.tools import create_now from opensipkd.tools import create_now, SaveFile
from opensipkd.tools.report import open_rml_row, csv_response, open_rml_pdf, pdf_response from opensipkd.tools.report import open_rml_row, csv_response, open_rml_pdf, pdf_response, file_response
from pyramid.i18n import TranslationStringFactory from pyramid.i18n import TranslationStringFactory
from pyramid.view import view_config from pyramid.view import view_config
from sqlalchemy import (func, ) from sqlalchemy import (func, )
...@@ -95,14 +95,17 @@ class Views(BaseView): ...@@ -95,14 +95,17 @@ class Views(BaseView):
departement=self.req.departement, departement=self.req.departement,
address=self.req.address, address=self.req.address,
base_path=base_path) base_path=base_path)
return pdf_response(self.req, pdf, filename) filename = os.path.basename(filename)
resp = pdf_response(self.req, pdf, filename)
# save_file = SaveFile('/tmp')
# r = save_file.save(pdf, filename=filename)
# resp = file_response(self.req, filename=r)
return resp
def form_validator(self, form, value): def form_validator(self, form, value):
if "company_id" in value and not value["company_id"]: if "company_id" in value and not value["company_id"]:
value["company_id"] = None value["company_id"] = None
def save_request(self, values, row=None): def save_request(self, values, row=None):
request = self.req request = self.req
values["email"] = values['email'].lower() values["email"] = values['email'].lower()
...@@ -156,11 +159,9 @@ class Views(BaseView): ...@@ -156,11 +159,9 @@ class Views(BaseView):
add_member_count(gid) add_member_count(gid)
return row return row
def after_add(self, row, values): def after_add(self, row, values):
pass pass
@view_config( @view_config(
route_name='user-add', renderer='templates/form.pt', route_name='user-add', renderer='templates/form.pt',
permission='user-view') permission='user-view')
...@@ -168,15 +169,13 @@ class Views(BaseView): ...@@ -168,15 +169,13 @@ class Views(BaseView):
return super(Views, self).view_add() return super(Views, self).view_add()
# user, remain = insert(request, values) # user, remain = insert(request, values)
def get_values(self, row, istime=False): def get_values(self, row, istime=False):
d = super(Views, self).get_values(row, istime) d = super(Views, self).get_values(row, istime)
d["groups"] = user_group_set(row) d["groups"] = user_group_set(row)
return d return d
@view_config( @view_config(
route_name='user-edit', renderer='templates/user/edit.pt', route_name='user-edit', renderer='templates/form.pt',
permission='user-edit') permission='user-edit')
def view_edit(self): def view_edit(self):
return super(Views, self).view_edit() return super(Views, self).view_edit()
...@@ -187,14 +186,12 @@ class Views(BaseView): ...@@ -187,14 +186,12 @@ class Views(BaseView):
def view_view(self): def view_view(self):
return super(Views, self).view_view() return super(Views, self).view_view()
@view_config( @view_config(
route_name='user-delete', renderer='templates/form.pt', route_name='user-delete', renderer='templates/form.pt',
permission='user-edit') permission='user-edit')
def view_delete(self): def view_delete(self):
return super(Views, self).view_delete() return super(Views, self).view_delete()
def delete_msg(self, row): def delete_msg(self, row):
data = dict(uid=row.id, email=row.email) data = dict(uid=row.id, email=row.email)
return _( return _(
...@@ -202,13 +199,11 @@ class Views(BaseView): ...@@ -202,13 +199,11 @@ class Views(BaseView):
default='User ${email} ID ${uid} has been deleted', default='User ${email} ID ${uid} has been deleted',
mapping=data) mapping=data)
def before_delete(self, row): def before_delete(self, row):
gid_list = user_group_set(row) gid_list = user_group_set(row)
for gid in gid_list: for gid in gid_list:
reduce_member_count(gid) reduce_member_count(gid)
def query_id(self): def query_id(self):
q = DBSession.query(User).filter_by(id=self.req.matchdict['id']) q = DBSession.query(User).filter_by(id=self.req.matchdict['id'])
if self.req.user.company_id: if self.req.user.company_id:
......
...@@ -281,37 +281,6 @@ def login(request, data): ...@@ -281,37 +281,6 @@ def login(request, data):
return login_(request, data) return login_(request, data)
# , permission='web-service'
def get_profile_(user):
# is_list = type(data) is list
# dat = is_list and data[0] or data
# user = get_user(data)
# if not user or not UserService.check_password(user, data['password']):
# raise JsonRpcInvalidLoginError
partner = Partner.query().filter_by(user_id=user.id).first()
if not partner:
raise JsonRpcInvalidDataError
result = dict(user_name=user.user_name,
nik=partner.kode,
email=partner.email,
mobile=partner.mobile,
nama=partner.nama, )
# result = is_list and [result] or result
return dict(data=result)
@jsonrpc_method(method='get_profile', endpoint='rpc-user')
def get_profile(request, data):
# Digunakan untuk memperoleh profile user
# parameter user_name, password
auth_from_rpc(request)
user = get_user(data)
if not user or not UserService.check_password(user, data['password']):
raise JsonRpcInvalidLoginError
return get_profile_(user)
# , permission='web-service' # , permission='web-service'
......
...@@ -116,7 +116,10 @@ class DeTable(field.Field): ...@@ -116,7 +116,10 @@ class DeTable(field.Field):
params = params and f"?{params}" or "" params = params and f"?{params}" or ""
btn_close_js = "{window.location = '/'; return false;}" btn_close_js = "{window.location = '/'; return false;}"
btn_add_js = "{window.location = o%sUri+'/add%s';}" % (tableid, params) btn_add_js = "{window.location = o%sUri+'/add%s';}" % (tableid, params)
btn_edit_js = "{window.location = o%sUri+'/'+m%sID+'/edit%s'}" % (tableid, tableid, params) btn_edit_js = """{
if (m%sID) window.location = o%sUri+'/'+m%sID+'/edit%s';
else alert('Pilih Baris');
}""" % (tableid, tableid, tableid, params)
btn_view_js = "{window.location = o%sUri+'/'+m%sID+'/view%s';}" % (tableid, tableid, params) btn_view_js = "{window.location = o%sUri+'/'+m%sID+'/view%s';}" % (tableid, tableid, params)
btn_delete_js = "{window.location = o%sUri+'/'+m%sID+'/delete%s';}" % (tableid, tableid, params) btn_delete_js = "{window.location = o%sUri+'/'+m%sID+'/delete%s';}" % (tableid, tableid, params)
btn_csv_js = "{window.location = o%sUri+'/csv/act%s';}" % (tableid, params) btn_csv_js = "{window.location = o%sUri+'/csv/act%s';}" % (tableid, params)
......
from pyramid_rpc.jsonrpc import jsonrpc_method import logging
import venusian
from pyramid.httpexceptions import HTTPFound
from pyramid.security import NO_PERMISSION_REQUIRED
from pyramid_rpc.jsonrpc import jsonrpc_method as json_rpc_base, MethodPredicate, BatchedRequestPredicate, \
EndpointPredicate, jsonrpc_renderer, DEFAULT_RENDERER, add_jsonrpc_endpoint, add_jsonrpc_method, JsonRpcError, \
exception_view, JsonRpcRequestInvalid, parse_request_GET, parse_request_POST
from opensipkd.base.tools.api import auth_from_rpc
from opensipkd.base.views.user_login import get_login_headers
log = logging.getLogger(__name__)
def setup_request(endpoint, request):
""" Parse a JSON-RPC request body."""
if request.method == 'GET':
parse_request_GET(request)
elif request.method == 'POST':
parse_request_POST(request)
else:
log.debug('unsupported request method "%s"', request.method)
raise JsonRpcRequestInvalid
if hasattr(request, 'batched_rpc_requests'):
log.debug('handling batched rpc request')
# the checks below will look at the subrequests
return
if request.rpc_version != '2.0':
log.debug('id:%s invalid rpc version %s',
request.rpc_id, request.rpc_version)
raise JsonRpcRequestInvalid
if request.rpc_method is None:
log.debug('id:%s invalid rpc method', request.rpc_id)
raise JsonRpcRequestInvalid
log.debug('handling id:%s method:%s',
request.rpc_id, request.rpc_method)
class MethodPredicate(object):
def __init__(self, val, config):
self.method = val
def text(self):
return 'jsonrpc method = %s' % self.method
phash = text
def __call__(self, context, request):
user = auth_from_rpc(request)
headers = get_login_headers(request, user)
response = HTTPFound(location=request.route_url('home'), headers=headers)
# response = request.response
request.response.set_cookie('userid', value=str(user.id), max_age=31536000) # max_age = year
return getattr(request, 'rpc_method', None) == self.method
def includeme(config):
""" Set up standard configurator registrations. Use via:
.. code-block:: python
config = Configurator()
config.include('pyramid_rpc.jsonrpc')
Once this function has been invoked, two new directives will be
available on the configurator:
- ``add_jsonrpc_endpoint``: Add an endpoint for handling JSON-RPC.
- ``add_jsonrpc_method``: Add a method to a JSON-RPC endpoint.
"""
if not hasattr(config.registry, 'jsonrpc_endpoints'):
config.registry.jsonrpc_endpoints = {}
config.add_view_predicate('jsonrpc_method', MethodPredicate)
config.add_view_predicate('jsonrpc_batched', BatchedRequestPredicate)
config.add_route_predicate('jsonrpc_endpoint', EndpointPredicate)
config.add_renderer(DEFAULT_RENDERER, jsonrpc_renderer)
config.add_directive('add_jsonrpc_endpoint', add_jsonrpc_endpoint)
config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
config.add_view(exception_view, context=JsonRpcError,
permission=NO_PERMISSION_REQUIRED)
sqlalchemy~=1.4.22 sqlalchemy~=1.4.22
zope.sqlalchemy
pyramid~=2.0 pyramid~=2.0
pyramid_beaker
colander~=1.8.3 colander~=1.8.3
deform~=2.0.15 deform~=2.0.15
transaction~=3.0.1 transaction~=3.0.1
colorama~=0.4.4 colorama~=0.4.4
Pillow~=8.3.1 Pillow>=9.1.1
lxml~=4.6.3 lxml>=4.9.0
beautifulsoup4~=4.9.3 beautifulsoup4>=4.11.1
soupsieve~=2.2.1 soupsieve>=2.3.2.post1
pip~=18.1 pip>=22.1.2
wheel~=0.37.0 wheel~=0.37.0
rsa~=4.7.2 rsa>=4.8
pyasn1~=0.4.8 pyasn1~=0.4.8
Chameleon~=3.9.1 Chameleon>=3.10.1
six~=1.16.0 six~=1.16.0
Mako~=1.1.4 Mako>=1.2.0
Babel~=2.9.1 Babel>=2.10.3
Beaker~=1.11.0 Beaker~=1.11.0
Pygments~=2.10.0 Pygments>=2.12.0
MarkupSafe~=2.0.1 MarkupSafe>=2.1.1
Genshi~=0.7.5 Genshi~=0.7.5
pytz~=2021.1 pytz>=2022.1
WebOb~=1.8.7 WebOb~=1.8.7
translationstring~=1.4 translationstring~=1.4
peppercorn~=0.6 peppercorn~=0.6
iso8601~=0.1.16 iso8601>=1.0.2
google~=3.0.0 google~=3.0.0
cachetools~=4.2.2 cachetools>=5.2.0
certifi~=2021.5.30 certifi>=2022.6.15
urllib3~=1.26.6 urllib3~=1.26.6
requests~=2.26.0 requests>=2.28.0
google-api-python-client~=2.19.0 google-api-python-client>=2.51.0
python-dateutil~=2.8.2 python-dateutil~=2.8.2
alembic~=1.6.5 alembic>=1.8.0
passlib~=1.7.4 passlib~=1.7.4
venusian~=3.0.0 venusian~=3.0.0
plaster~=1.0 plaster~=1.0
hupper~=1.10.3 hupper~=1.10.3
waitress~=2.0.0 waitress>=2.1.2
greenlet~=1.1.1 greenlet~=1.1.1
pyparsing~=2.4.7 pyparsing>=3.0.9
httplib2~=0.19.1 httplib2>=0.20.4
icecream~=2.1.1 icecream~=2.1.1
executing~=0.8.0 executing~=0.8.0
paginate~=0.5.6 paginate~=0.5.6
idna~=3.2 idna~=3.2
chardet~=4.0.0
asttokens~=2.0.5 asttokens~=2.0.5
setuptools~=56.0.0 setuptools>=57.4.0
uritemplate~=3.0.1 uritemplate>=4.1.1
zipp~=3.5.0
reportlab~=3.6.1 reportlab~=3.6.1
PyJWT~=2.1.0 PyJWT>=2.4.0
qrcode~=7.3.1
py~=1.11.0 py~=1.11.0
attrs~=21.4.0 attrs~=21.4.0
toml~=0.10.2
pytest~=7.1.1 pytest~=7.1.1
pluggy~=1.0.0 pluggy~=1.0.0
iniconfig~=1.1.1
\ No newline at end of file \ No newline at end of file
iniconfig~=1.1.1
cffi>=1.15.0
cryptography>=37.0.2
jwt>=1.3.1
pikepdf>=5.1.5
packaging>=21.3
pycparser>=2.21
pyramid_rpc
\ No newline at end of file \ No newline at end of file
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!