diff --git a/.gitignore b/.gitignore index 44c54156..e0bf6584 100644 --- a/.gitignore +++ b/.gitignore @@ -135,3 +135,47 @@ app/ch10_using_sqlachemy/final/.idea/dataSources.local.xml app/ch10_using_sqlachemy/final/.idea/dataSources/cfaa71de-14c4-40a6-abf7-fc59c06c9b0b.xml app/ch11_migrations/final/.idea/dataSources.local.xml app/ch11_migrations/final/.idea/dataSources/bb882f32-bdb3-45cb-adce-c46711dd125e.xml +.idea/$CACHE_FILE$ +.idea/flask-demos.iml +.idea/misc.xml +.idea/modules.xml +.idea/vagrant.xml +.idea/vcs.xml +.idea/inspectionProfiles/profiles_settings.xml +app/ch09_sqlalchemy/final/.idea/inspectionProfiles/profiles_settings.xml +app/ch09_sqlalchemy/final/.idea/inspectionProfiles/Project_Default.xml +app/ch10_using_sqlachemy/starter/.idea/$CACHE_FILE$ +app/ch10_using_sqlachemy/starter/.idea/.gitignore +app/ch10_using_sqlachemy/starter/.idea/.name +app/ch10_using_sqlachemy/starter/.idea/misc.xml +app/ch10_using_sqlachemy/starter/.idea/modules.xml +app/ch10_using_sqlachemy/starter/.idea/vagrant.xml +app/ch10_using_sqlachemy/starter/.idea/vcs.xml +app/ch10_using_sqlachemy/starter/.idea/inspectionProfiles/profiles_settings.xml +app/ch10_using_sqlachemy/starter/.idea/inspectionProfiles/Project_Default.xml +app/ch11_migrations/starter/.idea/$CACHE_FILE$ +app/ch11_migrations/starter/.idea/.gitignore +app/ch11_migrations/starter/.idea/.name +app/ch11_migrations/starter/.idea/misc.xml +app/ch11_migrations/starter/.idea/modules.xml +app/ch11_migrations/starter/.idea/vagrant.xml +app/ch11_migrations/starter/.idea/vcs.xml +app/ch11_migrations/starter/.idea/inspectionProfiles/profiles_settings.xml +app/ch11_migrations/starter/.idea/inspectionProfiles/Project_Default.xml +app/ch12-forms/starter/.idea/$CACHE_FILE$ +app/ch12-forms/starter/.idea/.gitignore +app/ch12-forms/starter/.idea/.name +app/ch12-forms/starter/.idea/misc.xml +app/ch12-forms/starter/.idea/modules.xml +app/ch12-forms/starter/.idea/vagrant.xml +app/ch12-forms/starter/.idea/vcs.xml +app/ch12-forms/starter/.idea/inspectionProfiles/profiles_settings.xml +app/ch12-forms/starter/.idea/inspectionProfiles/Project_Default.xml +app/ch13-validation/starter/.idea/inspectionProfiles/Project_Default.xml +app/ch14_testing/final/.idea/inspectionProfiles/Project_Default.xml +app/ch14_testing/starter/.idea/inspectionProfiles/Project_Default.xml +app/ch15_deploy/final/.idea/inspectionProfiles/Project_Default.xml +.idea/web-apps-flask-course.iml +.idea/inspectionProfiles/Project_Default.xml +/.idea/jsLibraryMappings.xml +/.idea/webResources.xml diff --git a/.idea/dictionaries/mkennedy.xml b/.idea/dictionaries/mkennedy.xml new file mode 100644 index 00000000..630ad4c3 --- /dev/null +++ b/.idea/dictionaries/mkennedy.xml @@ -0,0 +1,14 @@ + + + + dateutil + mimetype + multidict + passlib + pypi + sqlalchemy + tablename + werkzeug + + + \ No newline at end of file diff --git a/.idea/ruff.xml b/.idea/ruff.xml new file mode 100644 index 00000000..100ae2fc --- /dev/null +++ b/.idea/ruff.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/ch04_first_site/first_site_final/first_site/app.py b/app/ch04_first_site/first_site_final/first_site/app.py index 9946e943..63e99004 100644 --- a/app/ch04_first_site/first_site_final/first_site/app.py +++ b/app/ch04_first_site/first_site_final/first_site/app.py @@ -2,8 +2,10 @@ app = flask.Flask(__name__) + @app.route('/') def index(): - return "Hello world" + return 'Hello world' + app.run() diff --git a/app/ch04_first_site/first_site_final/first_site/requirements.piptools b/app/ch04_first_site/first_site_final/first_site/requirements.piptools new file mode 100644 index 00000000..7e106024 --- /dev/null +++ b/app/ch04_first_site/first_site_final/first_site/requirements.piptools @@ -0,0 +1 @@ +flask diff --git a/app/ch04_first_site/first_site_final/first_site/requirements.txt b/app/ch04_first_site/first_site_final/first_site/requirements.txt index 7e106024..eea1cfd5 100644 --- a/app/ch04_first_site/first_site_final/first_site/requirements.txt +++ b/app/ch04_first_site/first_site_final/first_site/requirements.txt @@ -1 +1,18 @@ -flask +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +markupsafe==3.0.2 + # via + # jinja2 + # werkzeug +werkzeug==3.1.3 + # via flask diff --git a/app/ch05_jinja_templates/final/pypi_org/app.py b/app/ch05_jinja_templates/final/pypi_org/app.py index fe54fabf..ba6389b2 100644 --- a/app/ch05_jinja_templates/final/pypi_org/app.py +++ b/app/ch05_jinja_templates/final/pypi_org/app.py @@ -1,6 +1,7 @@ import os import sys import flask + folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, folder) diff --git a/app/ch05_jinja_templates/final/pypi_org/infrastructure/view_modifiers.py b/app/ch05_jinja_templates/final/pypi_org/infrastructure/view_modifiers.py index 5bb6c178..d944c142 100644 --- a/app/ch05_jinja_templates/final/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch05_jinja_templates/final/pypi_org/infrastructure/view_modifiers.py @@ -1,6 +1,8 @@ from functools import wraps import flask +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -10,6 +12,10 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) + + if isinstance(response_val, werkzeug.wrappers.Response): + return response_val + if isinstance(response_val, flask.Response): return response_val @@ -20,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -36,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch05_jinja_templates/final/requirements.piptools b/app/ch05_jinja_templates/final/requirements.piptools new file mode 100644 index 00000000..7e106024 --- /dev/null +++ b/app/ch05_jinja_templates/final/requirements.piptools @@ -0,0 +1 @@ +flask diff --git a/app/ch05_jinja_templates/final/requirements.txt b/app/ch05_jinja_templates/final/requirements.txt index 7e106024..eea1cfd5 100644 --- a/app/ch05_jinja_templates/final/requirements.txt +++ b/app/ch05_jinja_templates/final/requirements.txt @@ -1 +1,18 @@ -flask +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +markupsafe==3.0.2 + # via + # jinja2 + # werkzeug +werkzeug==3.1.3 + # via flask diff --git a/app/ch06_routing/final/pypi_org/app.py b/app/ch06_routing/final/pypi_org/app.py index 8b78c217..7b493c47 100644 --- a/app/ch06_routing/final/pypi_org/app.py +++ b/app/ch06_routing/final/pypi_org/app.py @@ -1,6 +1,7 @@ import os import sys import flask + folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, folder) @@ -25,3 +26,5 @@ def register_blueprints(): if __name__ == '__main__': main() +else: + register_blueprints() diff --git a/app/ch06_routing/final/pypi_org/infrastructure/view_modifiers.py b/app/ch06_routing/final/pypi_org/infrastructure/view_modifiers.py index 5bb6c178..d944c142 100644 --- a/app/ch06_routing/final/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch06_routing/final/pypi_org/infrastructure/view_modifiers.py @@ -1,6 +1,8 @@ from functools import wraps import flask +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -10,6 +12,10 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) + + if isinstance(response_val, werkzeug.wrappers.Response): + return response_val + if isinstance(response_val, flask.Response): return response_val @@ -20,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -36,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch06_routing/final/pypi_org/views/account_views.py b/app/ch06_routing/final/pypi_org/views/account_views.py index 0421324a..55e5df41 100644 --- a/app/ch06_routing/final/pypi_org/views/account_views.py +++ b/app/ch06_routing/final/pypi_org/views/account_views.py @@ -16,6 +16,7 @@ def index(): # ################### REGISTER ################################# + @blueprint.route('/account/register', methods=['GET']) @response(template_file='account/register.html') def register_get(): @@ -30,6 +31,7 @@ def register_post(): # ################### LOGIN ################################# + @blueprint.route('/account/login', methods=['GET']) @response(template_file='account/login.html') def login_get(): @@ -44,6 +46,7 @@ def login_post(): # ################### LOGOUT ################################# + @blueprint.route('/account/logout') def logout(): return {} diff --git a/app/ch06_routing/final/pypi_org/views/cms_views.py b/app/ch06_routing/final/pypi_org/views/cms_views.py index f7172d3d..a8b755a8 100644 --- a/app/ch06_routing/final/pypi_org/views/cms_views.py +++ b/app/ch06_routing/final/pypi_org/views/cms_views.py @@ -9,7 +9,7 @@ @blueprint.route('/') @response(template_file='cms/page.html') def cms_page(full_url: str): - print("Getting CMS page for {}".format(full_url)) + print('Getting CMS page for {}'.format(full_url)) page = cms_service.get_page(full_url) if not page: diff --git a/app/ch06_routing/final/pypi_org/views/package_views.py b/app/ch06_routing/final/pypi_org/views/package_views.py index ccf6bf12..7450fa1c 100644 --- a/app/ch06_routing/final/pypi_org/views/package_views.py +++ b/app/ch06_routing/final/pypi_org/views/package_views.py @@ -9,10 +9,10 @@ @blueprint.route('/project/') # @response(template_file='packages/details.html') def package_details(package_name: str): - return "Package details for {}".format(package_name) + return 'Package details for {}'.format(package_name) @blueprint.route('/') def popular(rank: int): print(type(rank), rank) - return "The details for the {}th most popular package".format(rank) + return 'The details for the {}th most popular package'.format(rank) diff --git a/app/ch06_routing/final/requirements.piptools b/app/ch06_routing/final/requirements.piptools new file mode 100644 index 00000000..7e106024 --- /dev/null +++ b/app/ch06_routing/final/requirements.piptools @@ -0,0 +1 @@ +flask diff --git a/app/ch06_routing/final/requirements.txt b/app/ch06_routing/final/requirements.txt index 7e106024..eea1cfd5 100644 --- a/app/ch06_routing/final/requirements.txt +++ b/app/ch06_routing/final/requirements.txt @@ -1 +1,18 @@ -flask +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +markupsafe==3.0.2 + # via + # jinja2 + # werkzeug +werkzeug==3.1.3 + # via flask diff --git a/app/ch06_routing/starter/pypi_org/app.py b/app/ch06_routing/starter/pypi_org/app.py index fe54fabf..ba6389b2 100644 --- a/app/ch06_routing/starter/pypi_org/app.py +++ b/app/ch06_routing/starter/pypi_org/app.py @@ -1,6 +1,7 @@ import os import sys import flask + folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, folder) diff --git a/app/ch06_routing/starter/pypi_org/infrastructure/view_modifiers.py b/app/ch06_routing/starter/pypi_org/infrastructure/view_modifiers.py index 5bb6c178..d944c142 100644 --- a/app/ch06_routing/starter/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch06_routing/starter/pypi_org/infrastructure/view_modifiers.py @@ -1,6 +1,8 @@ from functools import wraps import flask +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -10,6 +12,10 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) + + if isinstance(response_val, werkzeug.wrappers.Response): + return response_val + if isinstance(response_val, flask.Response): return response_val @@ -20,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -36,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch06_routing/starter/requirements.piptools b/app/ch06_routing/starter/requirements.piptools new file mode 100644 index 00000000..7e106024 --- /dev/null +++ b/app/ch06_routing/starter/requirements.piptools @@ -0,0 +1 @@ +flask diff --git a/app/ch06_routing/starter/requirements.txt b/app/ch06_routing/starter/requirements.txt index 7e106024..eea1cfd5 100644 --- a/app/ch06_routing/starter/requirements.txt +++ b/app/ch06_routing/starter/requirements.txt @@ -1 +1,18 @@ -flask +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +markupsafe==3.0.2 + # via + # jinja2 + # werkzeug +werkzeug==3.1.3 + # via flask diff --git a/app/ch08_adding_our_design/final/pypi_org/app.py b/app/ch08_adding_our_design/final/pypi_org/app.py index bbd6bc9d..80344a30 100644 --- a/app/ch08_adding_our_design/final/pypi_org/app.py +++ b/app/ch08_adding_our_design/final/pypi_org/app.py @@ -1,6 +1,7 @@ import os import sys import flask + folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, folder) diff --git a/app/ch08_adding_our_design/final/pypi_org/infrastructure/view_modifiers.py b/app/ch08_adding_our_design/final/pypi_org/infrastructure/view_modifiers.py index 5bb6c178..d944c142 100644 --- a/app/ch08_adding_our_design/final/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch08_adding_our_design/final/pypi_org/infrastructure/view_modifiers.py @@ -1,6 +1,8 @@ from functools import wraps import flask +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -10,6 +12,10 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) + + if isinstance(response_val, werkzeug.wrappers.Response): + return response_val + if isinstance(response_val, flask.Response): return response_val @@ -20,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -36,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch08_adding_our_design/final/pypi_org/views/account_views.py b/app/ch08_adding_our_design/final/pypi_org/views/account_views.py index 0421324a..55e5df41 100644 --- a/app/ch08_adding_our_design/final/pypi_org/views/account_views.py +++ b/app/ch08_adding_our_design/final/pypi_org/views/account_views.py @@ -16,6 +16,7 @@ def index(): # ################### REGISTER ################################# + @blueprint.route('/account/register', methods=['GET']) @response(template_file='account/register.html') def register_get(): @@ -30,6 +31,7 @@ def register_post(): # ################### LOGIN ################################# + @blueprint.route('/account/login', methods=['GET']) @response(template_file='account/login.html') def login_get(): @@ -44,6 +46,7 @@ def login_post(): # ################### LOGOUT ################################# + @blueprint.route('/account/logout') def logout(): return {} diff --git a/app/ch08_adding_our_design/final/pypi_org/views/cms_views.py b/app/ch08_adding_our_design/final/pypi_org/views/cms_views.py index f7172d3d..a8b755a8 100644 --- a/app/ch08_adding_our_design/final/pypi_org/views/cms_views.py +++ b/app/ch08_adding_our_design/final/pypi_org/views/cms_views.py @@ -9,7 +9,7 @@ @blueprint.route('/') @response(template_file='cms/page.html') def cms_page(full_url: str): - print("Getting CMS page for {}".format(full_url)) + print('Getting CMS page for {}'.format(full_url)) page = cms_service.get_page(full_url) if not page: diff --git a/app/ch08_adding_our_design/final/pypi_org/views/package_views.py b/app/ch08_adding_our_design/final/pypi_org/views/package_views.py index ccf6bf12..7450fa1c 100644 --- a/app/ch08_adding_our_design/final/pypi_org/views/package_views.py +++ b/app/ch08_adding_our_design/final/pypi_org/views/package_views.py @@ -9,10 +9,10 @@ @blueprint.route('/project/') # @response(template_file='packages/details.html') def package_details(package_name: str): - return "Package details for {}".format(package_name) + return 'Package details for {}'.format(package_name) @blueprint.route('/') def popular(rank: int): print(type(rank), rank) - return "The details for the {}th most popular package".format(rank) + return 'The details for the {}th most popular package'.format(rank) diff --git a/app/ch08_adding_our_design/final/requirements.piptools b/app/ch08_adding_our_design/final/requirements.piptools new file mode 100644 index 00000000..7e106024 --- /dev/null +++ b/app/ch08_adding_our_design/final/requirements.piptools @@ -0,0 +1 @@ +flask diff --git a/app/ch08_adding_our_design/final/requirements.txt b/app/ch08_adding_our_design/final/requirements.txt index 7e106024..eea1cfd5 100644 --- a/app/ch08_adding_our_design/final/requirements.txt +++ b/app/ch08_adding_our_design/final/requirements.txt @@ -1 +1,18 @@ -flask +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +markupsafe==3.0.2 + # via + # jinja2 + # werkzeug +werkzeug==3.1.3 + # via flask diff --git a/app/ch08_adding_our_design/starter/pypi_org/app.py b/app/ch08_adding_our_design/starter/pypi_org/app.py index bbd6bc9d..80344a30 100644 --- a/app/ch08_adding_our_design/starter/pypi_org/app.py +++ b/app/ch08_adding_our_design/starter/pypi_org/app.py @@ -1,6 +1,7 @@ import os import sys import flask + folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, folder) diff --git a/app/ch08_adding_our_design/starter/pypi_org/infrastructure/view_modifiers.py b/app/ch08_adding_our_design/starter/pypi_org/infrastructure/view_modifiers.py index 5bb6c178..d944c142 100644 --- a/app/ch08_adding_our_design/starter/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch08_adding_our_design/starter/pypi_org/infrastructure/view_modifiers.py @@ -1,6 +1,8 @@ from functools import wraps import flask +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -10,6 +12,10 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) + + if isinstance(response_val, werkzeug.wrappers.Response): + return response_val + if isinstance(response_val, flask.Response): return response_val @@ -20,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -36,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch08_adding_our_design/starter/pypi_org/views/account_views.py b/app/ch08_adding_our_design/starter/pypi_org/views/account_views.py index 0421324a..55e5df41 100644 --- a/app/ch08_adding_our_design/starter/pypi_org/views/account_views.py +++ b/app/ch08_adding_our_design/starter/pypi_org/views/account_views.py @@ -16,6 +16,7 @@ def index(): # ################### REGISTER ################################# + @blueprint.route('/account/register', methods=['GET']) @response(template_file='account/register.html') def register_get(): @@ -30,6 +31,7 @@ def register_post(): # ################### LOGIN ################################# + @blueprint.route('/account/login', methods=['GET']) @response(template_file='account/login.html') def login_get(): @@ -44,6 +46,7 @@ def login_post(): # ################### LOGOUT ################################# + @blueprint.route('/account/logout') def logout(): return {} diff --git a/app/ch08_adding_our_design/starter/pypi_org/views/cms_views.py b/app/ch08_adding_our_design/starter/pypi_org/views/cms_views.py index f7172d3d..a8b755a8 100644 --- a/app/ch08_adding_our_design/starter/pypi_org/views/cms_views.py +++ b/app/ch08_adding_our_design/starter/pypi_org/views/cms_views.py @@ -9,7 +9,7 @@ @blueprint.route('/') @response(template_file='cms/page.html') def cms_page(full_url: str): - print("Getting CMS page for {}".format(full_url)) + print('Getting CMS page for {}'.format(full_url)) page = cms_service.get_page(full_url) if not page: diff --git a/app/ch08_adding_our_design/starter/pypi_org/views/package_views.py b/app/ch08_adding_our_design/starter/pypi_org/views/package_views.py index ccf6bf12..7450fa1c 100644 --- a/app/ch08_adding_our_design/starter/pypi_org/views/package_views.py +++ b/app/ch08_adding_our_design/starter/pypi_org/views/package_views.py @@ -9,10 +9,10 @@ @blueprint.route('/project/') # @response(template_file='packages/details.html') def package_details(package_name: str): - return "Package details for {}".format(package_name) + return 'Package details for {}'.format(package_name) @blueprint.route('/') def popular(rank: int): print(type(rank), rank) - return "The details for the {}th most popular package".format(rank) + return 'The details for the {}th most popular package'.format(rank) diff --git a/app/ch08_adding_our_design/starter/requirements.piptools b/app/ch08_adding_our_design/starter/requirements.piptools new file mode 100644 index 00000000..7e106024 --- /dev/null +++ b/app/ch08_adding_our_design/starter/requirements.piptools @@ -0,0 +1 @@ +flask diff --git a/app/ch08_adding_our_design/starter/requirements.txt b/app/ch08_adding_our_design/starter/requirements.txt index 7e106024..eea1cfd5 100644 --- a/app/ch08_adding_our_design/starter/requirements.txt +++ b/app/ch08_adding_our_design/starter/requirements.txt @@ -1 +1,18 @@ -flask +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +markupsafe==3.0.2 + # via + # jinja2 + # werkzeug +werkzeug==3.1.3 + # via flask diff --git a/app/ch09_sqlalchemy/final/.idea/.name b/app/ch09_sqlalchemy/final/.idea/.name index b10607c3..a19c1f1a 100644 --- a/app/ch09_sqlalchemy/final/.idea/.name +++ b/app/ch09_sqlalchemy/final/.idea/.name @@ -1 +1 @@ -sqlalchemy \ No newline at end of file +flask ch09_sqlalchemy - final \ No newline at end of file diff --git a/app/ch09_sqlalchemy/final/.idea/flask ch09_sqlalchemy - final.iml b/app/ch09_sqlalchemy/final/.idea/flask ch09_sqlalchemy - final.iml new file mode 100644 index 00000000..7f037c06 --- /dev/null +++ b/app/ch09_sqlalchemy/final/.idea/flask ch09_sqlalchemy - final.iml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/ch09_sqlalchemy/final/pypi_org/app.py b/app/ch09_sqlalchemy/final/pypi_org/app.py index 4b39b374..0315d74a 100644 --- a/app/ch09_sqlalchemy/final/pypi_org/app.py +++ b/app/ch09_sqlalchemy/final/pypi_org/app.py @@ -1,6 +1,7 @@ import os import sys import flask + folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, folder) @@ -16,10 +17,7 @@ def main(): def setup_db(): - db_file = os.path.join( - os.path.dirname(__file__), - 'db', - 'pypi.sqlite') + db_file = os.path.join(os.path.dirname(__file__), 'db', 'pypi.sqlite') db_session.global_init(db_file) diff --git a/app/ch09_sqlalchemy/final/pypi_org/data/__all_models.py b/app/ch09_sqlalchemy/final/pypi_org/data/__all_models.py index 6c137449..1b24d092 100644 --- a/app/ch09_sqlalchemy/final/pypi_org/data/__all_models.py +++ b/app/ch09_sqlalchemy/final/pypi_org/data/__all_models.py @@ -5,15 +5,21 @@ # noinspection PyUnresolvedReferences import pypi_org.data.downloads + # noinspection PyUnresolvedReferences import pypi_org.data.languages + # noinspection PyUnresolvedReferences import pypi_org.data.licenses + # noinspection PyUnresolvedReferences import pypi_org.data.maintainers + # noinspection PyUnresolvedReferences import pypi_org.data.package + # noinspection PyUnresolvedReferences import pypi_org.data.releases + # noinspection PyUnresolvedReferences import pypi_org.data.users diff --git a/app/ch09_sqlalchemy/final/pypi_org/data/db_session.py b/app/ch09_sqlalchemy/final/pypi_org/data/db_session.py index 24973d64..4a39c10f 100644 --- a/app/ch09_sqlalchemy/final/pypi_org/data/db_session.py +++ b/app/ch09_sqlalchemy/final/pypi_org/data/db_session.py @@ -13,12 +13,15 @@ def global_init(db_file: str): return if not db_file or not db_file.strip(): - raise Exception("You must specify a db file.") + raise Exception('You must specify a db file.') conn_str = 'sqlite:///' + db_file.strip() - print("Connecting to DB with {}".format(conn_str)) + print('Connecting to DB with {}'.format(conn_str)) - engine = sa.create_engine(conn_str, echo=False) + # Adding check_same_thread = False after the recording. This can be an issue about + # creating / owner thread when cleaning up sessions, etc. This is a sqlite restriction + # that we probably don't care about in this example. + engine = sa.create_engine(conn_str, echo=False, connect_args={'check_same_thread': False}) factory = orm.sessionmaker(bind=engine) # noinspection PyUnresolvedReferences diff --git a/app/ch09_sqlalchemy/final/pypi_org/data/package.py b/app/ch09_sqlalchemy/final/pypi_org/data/package.py index d3806ea3..a272243d 100644 --- a/app/ch09_sqlalchemy/final/pypi_org/data/package.py +++ b/app/ch09_sqlalchemy/final/pypi_org/data/package.py @@ -3,6 +3,8 @@ import sqlalchemy as sa import sqlalchemy.orm as orm +from sqlalchemy.orm import Mapped + from pypi_org.data.modelbase import SqlAlchemyBase from pypi_org.data.releases import Release @@ -25,11 +27,15 @@ class Package(SqlAlchemyBase): license = sa.Column(sa.String, index=True) # releases relationship - releases: List[Release] = orm.relation("Release", order_by=[ - Release.major_ver.desc(), - Release.minor_ver.desc(), - Release.build_ver.desc(), - ], back_populates='package') + releases: Mapped[Release] = orm.relationship( + 'Release', + order_by=[ + Release.major_ver.desc(), + Release.minor_ver.desc(), + Release.build_ver.desc(), + ], + back_populates='package', + ) def __repr__(self): return ''.format(self.id) diff --git a/app/ch09_sqlalchemy/final/pypi_org/data/releases.py b/app/ch09_sqlalchemy/final/pypi_org/data/releases.py index 682fa08a..a0be5918 100644 --- a/app/ch09_sqlalchemy/final/pypi_org/data/releases.py +++ b/app/ch09_sqlalchemy/final/pypi_org/data/releases.py @@ -19,11 +19,9 @@ class Release(SqlAlchemyBase): size = sqlalchemy.Column(sqlalchemy.BigInteger) # Package relationship - package_id = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey("packages.id")) - package = orm.relation('Package') + package_id = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey('packages.id')) + package = orm.relationship('Package') @property def version_text(self): return '{}.{}.{}'.format(self.major_ver, self.minor_ver, self.build_ver) - - diff --git a/app/ch09_sqlalchemy/final/pypi_org/infrastructure/view_modifiers.py b/app/ch09_sqlalchemy/final/pypi_org/infrastructure/view_modifiers.py index 5bb6c178..d944c142 100644 --- a/app/ch09_sqlalchemy/final/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch09_sqlalchemy/final/pypi_org/infrastructure/view_modifiers.py @@ -1,6 +1,8 @@ from functools import wraps import flask +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -10,6 +12,10 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) + + if isinstance(response_val, werkzeug.wrappers.Response): + return response_val + if isinstance(response_val, flask.Response): return response_val @@ -20,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -36,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch09_sqlalchemy/final/pypi_org/views/account_views.py b/app/ch09_sqlalchemy/final/pypi_org/views/account_views.py index 0421324a..55e5df41 100644 --- a/app/ch09_sqlalchemy/final/pypi_org/views/account_views.py +++ b/app/ch09_sqlalchemy/final/pypi_org/views/account_views.py @@ -16,6 +16,7 @@ def index(): # ################### REGISTER ################################# + @blueprint.route('/account/register', methods=['GET']) @response(template_file='account/register.html') def register_get(): @@ -30,6 +31,7 @@ def register_post(): # ################### LOGIN ################################# + @blueprint.route('/account/login', methods=['GET']) @response(template_file='account/login.html') def login_get(): @@ -44,6 +46,7 @@ def login_post(): # ################### LOGOUT ################################# + @blueprint.route('/account/logout') def logout(): return {} diff --git a/app/ch09_sqlalchemy/final/pypi_org/views/cms_views.py b/app/ch09_sqlalchemy/final/pypi_org/views/cms_views.py index f7172d3d..a8b755a8 100644 --- a/app/ch09_sqlalchemy/final/pypi_org/views/cms_views.py +++ b/app/ch09_sqlalchemy/final/pypi_org/views/cms_views.py @@ -9,7 +9,7 @@ @blueprint.route('/') @response(template_file='cms/page.html') def cms_page(full_url: str): - print("Getting CMS page for {}".format(full_url)) + print('Getting CMS page for {}'.format(full_url)) page = cms_service.get_page(full_url) if not page: diff --git a/app/ch09_sqlalchemy/final/pypi_org/views/package_views.py b/app/ch09_sqlalchemy/final/pypi_org/views/package_views.py index ccf6bf12..7450fa1c 100644 --- a/app/ch09_sqlalchemy/final/pypi_org/views/package_views.py +++ b/app/ch09_sqlalchemy/final/pypi_org/views/package_views.py @@ -9,10 +9,10 @@ @blueprint.route('/project/') # @response(template_file='packages/details.html') def package_details(package_name: str): - return "Package details for {}".format(package_name) + return 'Package details for {}'.format(package_name) @blueprint.route('/') def popular(rank: int): print(type(rank), rank) - return "The details for the {}th most popular package".format(rank) + return 'The details for the {}th most popular package'.format(rank) diff --git a/app/ch09_sqlalchemy/final/requirements.piptools b/app/ch09_sqlalchemy/final/requirements.piptools new file mode 100644 index 00000000..e39cea14 --- /dev/null +++ b/app/ch09_sqlalchemy/final/requirements.piptools @@ -0,0 +1,2 @@ +flask +sqlalchemy diff --git a/app/ch09_sqlalchemy/final/requirements.txt b/app/ch09_sqlalchemy/final/requirements.txt index e39cea14..25908d2b 100644 --- a/app/ch09_sqlalchemy/final/requirements.txt +++ b/app/ch09_sqlalchemy/final/requirements.txt @@ -1,2 +1,22 @@ -flask -sqlalchemy +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +markupsafe==3.0.2 + # via + # jinja2 + # werkzeug +sqlalchemy==2.0.38 + # via -r requirements.piptools +typing-extensions==4.12.2 + # via sqlalchemy +werkzeug==3.1.3 + # via flask diff --git a/app/ch09_sqlalchemy/starter/pypi_org/app.py b/app/ch09_sqlalchemy/starter/pypi_org/app.py index 8b78c217..5ac0607d 100644 --- a/app/ch09_sqlalchemy/starter/pypi_org/app.py +++ b/app/ch09_sqlalchemy/starter/pypi_org/app.py @@ -1,6 +1,7 @@ import os import sys import flask + folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, folder) diff --git a/app/ch09_sqlalchemy/starter/pypi_org/db/placeholder.txt b/app/ch09_sqlalchemy/starter/pypi_org/db/placeholder.txt new file mode 100644 index 00000000..f7c55980 --- /dev/null +++ b/app/ch09_sqlalchemy/starter/pypi_org/db/placeholder.txt @@ -0,0 +1 @@ +Just here so git will create this folder. \ No newline at end of file diff --git a/app/ch09_sqlalchemy/starter/pypi_org/infrastructure/view_modifiers.py b/app/ch09_sqlalchemy/starter/pypi_org/infrastructure/view_modifiers.py index 5bb6c178..d944c142 100644 --- a/app/ch09_sqlalchemy/starter/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch09_sqlalchemy/starter/pypi_org/infrastructure/view_modifiers.py @@ -1,6 +1,8 @@ from functools import wraps import flask +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -10,6 +12,10 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) + + if isinstance(response_val, werkzeug.wrappers.Response): + return response_val + if isinstance(response_val, flask.Response): return response_val @@ -20,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -36,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch09_sqlalchemy/starter/pypi_org/views/account_views.py b/app/ch09_sqlalchemy/starter/pypi_org/views/account_views.py index 0421324a..55e5df41 100644 --- a/app/ch09_sqlalchemy/starter/pypi_org/views/account_views.py +++ b/app/ch09_sqlalchemy/starter/pypi_org/views/account_views.py @@ -16,6 +16,7 @@ def index(): # ################### REGISTER ################################# + @blueprint.route('/account/register', methods=['GET']) @response(template_file='account/register.html') def register_get(): @@ -30,6 +31,7 @@ def register_post(): # ################### LOGIN ################################# + @blueprint.route('/account/login', methods=['GET']) @response(template_file='account/login.html') def login_get(): @@ -44,6 +46,7 @@ def login_post(): # ################### LOGOUT ################################# + @blueprint.route('/account/logout') def logout(): return {} diff --git a/app/ch09_sqlalchemy/starter/pypi_org/views/cms_views.py b/app/ch09_sqlalchemy/starter/pypi_org/views/cms_views.py index f7172d3d..a8b755a8 100644 --- a/app/ch09_sqlalchemy/starter/pypi_org/views/cms_views.py +++ b/app/ch09_sqlalchemy/starter/pypi_org/views/cms_views.py @@ -9,7 +9,7 @@ @blueprint.route('/') @response(template_file='cms/page.html') def cms_page(full_url: str): - print("Getting CMS page for {}".format(full_url)) + print('Getting CMS page for {}'.format(full_url)) page = cms_service.get_page(full_url) if not page: diff --git a/app/ch09_sqlalchemy/starter/pypi_org/views/package_views.py b/app/ch09_sqlalchemy/starter/pypi_org/views/package_views.py index ccf6bf12..7450fa1c 100644 --- a/app/ch09_sqlalchemy/starter/pypi_org/views/package_views.py +++ b/app/ch09_sqlalchemy/starter/pypi_org/views/package_views.py @@ -9,10 +9,10 @@ @blueprint.route('/project/') # @response(template_file='packages/details.html') def package_details(package_name: str): - return "Package details for {}".format(package_name) + return 'Package details for {}'.format(package_name) @blueprint.route('/') def popular(rank: int): print(type(rank), rank) - return "The details for the {}th most popular package".format(rank) + return 'The details for the {}th most popular package'.format(rank) diff --git a/app/ch09_sqlalchemy/starter/requirements.piptools b/app/ch09_sqlalchemy/starter/requirements.piptools new file mode 100644 index 00000000..7e106024 --- /dev/null +++ b/app/ch09_sqlalchemy/starter/requirements.piptools @@ -0,0 +1 @@ +flask diff --git a/app/ch09_sqlalchemy/starter/requirements.txt b/app/ch09_sqlalchemy/starter/requirements.txt index 7e106024..eea1cfd5 100644 --- a/app/ch09_sqlalchemy/starter/requirements.txt +++ b/app/ch09_sqlalchemy/starter/requirements.txt @@ -1 +1,18 @@ -flask +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +markupsafe==3.0.2 + # via + # jinja2 + # werkzeug +werkzeug==3.1.3 + # via flask diff --git a/app/ch10_using_sqlachemy/final/.idea/.name b/app/ch10_using_sqlachemy/final/.idea/.name index 32757fc9..7639e47c 100644 --- a/app/ch10_using_sqlachemy/final/.idea/.name +++ b/app/ch10_using_sqlachemy/final/.idea/.name @@ -1 +1 @@ -using_sqlachemy \ No newline at end of file +flask ch10_using_sqlachemy - final \ No newline at end of file diff --git a/app/ch10_using_sqlachemy/final/.idea/flask ch10_using_sqlachemy - final.iml b/app/ch10_using_sqlachemy/final/.idea/flask ch10_using_sqlachemy - final.iml new file mode 100644 index 00000000..31330bb6 --- /dev/null +++ b/app/ch10_using_sqlachemy/final/.idea/flask ch10_using_sqlachemy - final.iml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/ch10_using_sqlachemy/final/.idea/inspectionProfiles/Project_Default.xml b/app/ch10_using_sqlachemy/final/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..05d4a622 --- /dev/null +++ b/app/ch10_using_sqlachemy/final/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,21 @@ + + + + \ No newline at end of file diff --git a/app/ch10_using_sqlachemy/final/.idea/misc.xml b/app/ch10_using_sqlachemy/final/.idea/misc.xml index aa984a42..a74a0a3b 100644 --- a/app/ch10_using_sqlachemy/final/.idea/misc.xml +++ b/app/ch10_using_sqlachemy/final/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/app/ch10_using_sqlachemy/final/.idea/modules.xml b/app/ch10_using_sqlachemy/final/.idea/modules.xml index bd757f42..dd7bdfdf 100644 --- a/app/ch10_using_sqlachemy/final/.idea/modules.xml +++ b/app/ch10_using_sqlachemy/final/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/app/ch10_using_sqlachemy/final/pypi_org/app.py b/app/ch10_using_sqlachemy/final/pypi_org/app.py index 4b39b374..0315d74a 100644 --- a/app/ch10_using_sqlachemy/final/pypi_org/app.py +++ b/app/ch10_using_sqlachemy/final/pypi_org/app.py @@ -1,6 +1,7 @@ import os import sys import flask + folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, folder) @@ -16,10 +17,7 @@ def main(): def setup_db(): - db_file = os.path.join( - os.path.dirname(__file__), - 'db', - 'pypi.sqlite') + db_file = os.path.join(os.path.dirname(__file__), 'db', 'pypi.sqlite') db_session.global_init(db_file) diff --git a/app/ch10_using_sqlachemy/final/pypi_org/bin/basic_inserts.py b/app/ch10_using_sqlachemy/final/pypi_org/bin/basic_inserts.py index 3a2f360d..6d04f551 100644 --- a/app/ch10_using_sqlachemy/final/pypi_org/bin/basic_inserts.py +++ b/app/ch10_using_sqlachemy/final/pypi_org/bin/basic_inserts.py @@ -1,14 +1,13 @@ import os import sys +# Make it run more easily outside of PyCharm +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + import pypi_org.data.db_session as db_session from pypi_org.data.package import Package from pypi_org.data.releases import Release -# Make it run more easily outside of PyCharm -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) - def main(): init_db() @@ -20,24 +19,24 @@ def insert_a_package(): p = Package() p.id = input('Package id / name: ').strip().lower() - p.summary = input("Package summary: ").strip() - p.author_name = input("Author: ").strip() - p.license = input("License: ").strip() + p.summary = input('Package summary: ').strip() + p.author_name = input('Author: ').strip() + p.license = input('License: ').strip() - print("Release 1:") + print('Release 1:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) - print("Release 2:") + print('Release 2:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) session = db_session.create_session() diff --git a/app/ch10_using_sqlachemy/final/pypi_org/bin/load_data.py b/app/ch10_using_sqlachemy/final/pypi_org/bin/load_data.py index 15237f18..74c45c15 100644 --- a/app/ch10_using_sqlachemy/final/pypi_org/bin/load_data.py +++ b/app/ch10_using_sqlachemy/final/pypi_org/bin/load_data.py @@ -4,12 +4,15 @@ import time from typing import List, Optional, Dict +# This is listed as progressbar2: +# noinspection PyPackageRequirements,PyPackageRequirements import progressbar from dateutil.parser import parse -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) +# Make sure we can import pypi_org.* +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) +from pypi_org.infrastructure.num_convert import try_int import pypi_org.data.db_session as db_session from pypi_org.data.languages import ProgrammingLanguage from pypi_org.data.licenses import License @@ -39,7 +42,7 @@ def main(): def do_import_languages(file_data: List[dict]): imported = set() - print("Importing languages ... ", flush=True) + print('Importing languages ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -74,7 +77,7 @@ def do_import_languages(file_data: List[dict]): def do_import_licenses(file_data: List[dict]): imported = set() - print("Importing licenses ... ", flush=True) + print('Importing licenses ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -100,17 +103,17 @@ def do_import_licenses(file_data: List[dict]): def do_summary(): session = db_session.create_session() - print("Final numbers:") - print("Users: {:,}".format(session.query(User).count())) - print("Packages: {:,}".format(session.query(Package).count())) - print("Releases: {:,}".format(session.query(Release).count())) - print("Maintainers: {:,}".format(session.query(Maintainer).count())) - print("Languages: {:,}".format(session.query(ProgrammingLanguage).count())) - print("Licenses: {:,}".format(session.query(License).count())) + print('Final numbers:') + print('Users: {:,}'.format(session.query(User).count())) + print('Packages: {:,}'.format(session.query(Package).count())) + print('Releases: {:,}'.format(session.query(Release).count())) + print('Maintainers: {:,}'.format(session.query(Maintainer).count())) + print('Languages: {:,}'.format(session.query(ProgrammingLanguage).count())) + print('Licenses: {:,}'.format(session.query(License).count())) def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: - print("Importing users ... ", flush=True) + print('Importing users ... ', flush=True) with progressbar.ProgressBar(max_value=len(user_lookup)) as bar: for idx, (email, name) in enumerate(user_lookup.items()): session = db_session.create_session() @@ -134,29 +137,29 @@ def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: def do_import_packages(file_data: List[dict], user_lookup: Dict[str, User]): errored_packages = [] - print("Importing packages and releases ... ", flush=True) + print('Importing packages and releases ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): try: load_package(p, user_lookup) bar.update(idx) except Exception as x: - errored_packages.append((p, " *** Errored out for package {}, {}".format(p.get('package_name'), x))) + errored_packages.append((p, ' *** Errored out for package {}, {}'.format(p.get('package_name'), x))) raise sys.stderr.flush() sys.stdout.flush() print() - print("Completed packages with {} errors.".format(len(errored_packages))) - for (p, txt) in errored_packages: + print('Completed packages with {} errors.'.format(len(errored_packages))) + for p, txt in errored_packages: print(txt) def do_load_files() -> List[dict]: data_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../../data/pypi-top-100')) - print("Loading files from {}".format(data_path)) + print('Loading files from {}'.format(data_path)) files = get_file_names(data_path) - print("Found {:,} files, loading ...".format(len(files)), flush=True) - time.sleep(.1) + print('Found {:,} files, loading ...'.format(len(files)), flush=True) + time.sleep(0.1) file_data = [] with progressbar.ProgressBar(max_value=len(files)) as bar: @@ -171,7 +174,7 @@ def do_load_files() -> List[dict]: def find_users(data: List[dict]) -> dict: - print("Discovering users...", flush=True) + print('Discovering users...', flush=True) found_users = {} with progressbar.ProgressBar(max_value=len(data)) as bar: @@ -184,7 +187,7 @@ def find_users(data: List[dict]) -> dict: sys.stderr.flush() sys.stdout.flush() print() - print("Discovered {:,} users".format(len(found_users))) + print('Discovered {:,} users'.format(len(found_users))) print() return found_users @@ -215,7 +218,7 @@ def load_file_data(filename: str) -> dict: with open(filename, 'r', encoding='utf-8') as fin: data = json.load(fin) except Exception as x: - print("ERROR in file: {}, details: {}".format(filename, x), flush=True) + print('ERROR in file: {}, details: {}'.format(filename, x), flush=True) raise return data @@ -233,7 +236,7 @@ def load_package(data: dict, user_lookup: Dict[str, User]): p.author = info.get('author') p.author_email = info.get('author_email') - releases = build_releases(p.id, data.get("releases", {})) + releases = build_releases(p.id, data.get('releases', {})) if releases: p.created_date = releases[0].created_date @@ -283,18 +286,13 @@ def detect_license(license_text: str) -> Optional[str]: license_text = license_text.strip() if len(license_text) > 100 or '\n' in license_text: - return "CUSTOM" + return 'CUSTOM' - license_text = license_text \ - .replace('Software License', '') \ - .replace('License', '') + license_text = license_text.replace('Software License', '').replace('License', '') if '::' in license_text: # E.g. 'License :: OSI Approved :: Apache Software License' - return license_text \ - .split(':')[-1] \ - .replace(' ', ' ') \ - .strip() + return license_text.split(':')[-1].replace(' ', ' ').strip() return license_text.strip() @@ -339,13 +337,6 @@ def make_version_num(version_text): return major, minor, build -def try_int(text) -> int: - try: - return int(text) - except: - return 0 - - def init_db(): top_folder = os.path.dirname(__file__) rel_file = os.path.join('..', 'db', 'pypi.sqlite') @@ -357,9 +348,7 @@ def get_file_names(data_path: str) -> List[str]: files = [] for f in os.listdir(data_path): if f.endswith('.json'): - files.append( - os.path.abspath(os.path.join(data_path, f)) - ) + files.append(os.path.abspath(os.path.join(data_path, f))) files.sort() return files diff --git a/app/ch10_using_sqlachemy/final/pypi_org/data/__all_models.py b/app/ch10_using_sqlachemy/final/pypi_org/data/__all_models.py index 6c137449..1b24d092 100644 --- a/app/ch10_using_sqlachemy/final/pypi_org/data/__all_models.py +++ b/app/ch10_using_sqlachemy/final/pypi_org/data/__all_models.py @@ -5,15 +5,21 @@ # noinspection PyUnresolvedReferences import pypi_org.data.downloads + # noinspection PyUnresolvedReferences import pypi_org.data.languages + # noinspection PyUnresolvedReferences import pypi_org.data.licenses + # noinspection PyUnresolvedReferences import pypi_org.data.maintainers + # noinspection PyUnresolvedReferences import pypi_org.data.package + # noinspection PyUnresolvedReferences import pypi_org.data.releases + # noinspection PyUnresolvedReferences import pypi_org.data.users diff --git a/app/ch10_using_sqlachemy/final/pypi_org/data/db_session.py b/app/ch10_using_sqlachemy/final/pypi_org/data/db_session.py index 50861b3b..62391b85 100644 --- a/app/ch10_using_sqlachemy/final/pypi_org/data/db_session.py +++ b/app/ch10_using_sqlachemy/final/pypi_org/data/db_session.py @@ -14,12 +14,15 @@ def global_init(db_file: str): return if not db_file or not db_file.strip(): - raise Exception("You must specify a db file.") + raise Exception('You must specify a db file.') conn_str = 'sqlite:///' + db_file.strip() - print("Connecting to DB with {}".format(conn_str)) + print('Connecting to DB with {}'.format(conn_str)) - engine = sa.create_engine(conn_str, echo=False) + # Adding check_same_thread = False after the recording. This can be an issue about + # creating / owner thread when cleaning up sessions, etc. This is a sqlite restriction + # that we probably don't care about in this example. + engine = sa.create_engine(conn_str, echo=False, connect_args={'check_same_thread': False}) __factory = orm.sessionmaker(bind=engine) # noinspection PyUnresolvedReferences diff --git a/app/ch10_using_sqlachemy/final/pypi_org/data/downloads.py b/app/ch10_using_sqlachemy/final/pypi_org/data/downloads.py index 66068f02..dd8afd65 100644 --- a/app/ch10_using_sqlachemy/final/pypi_org/data/downloads.py +++ b/app/ch10_using_sqlachemy/final/pypi_org/data/downloads.py @@ -7,8 +7,7 @@ class Download(SqlAlchemyBase): __tablename__ = 'downloads' id: int = sqlalchemy.Column(sqlalchemy.BigInteger, primary_key=True, autoincrement=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) package_id: str = sqlalchemy.Column(sqlalchemy.String, index=True) release_id: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) diff --git a/app/ch10_using_sqlachemy/final/pypi_org/data/languages.py b/app/ch10_using_sqlachemy/final/pypi_org/data/languages.py index 964b8444..3a73dde0 100644 --- a/app/ch10_using_sqlachemy/final/pypi_org/data/languages.py +++ b/app/ch10_using_sqlachemy/final/pypi_org/data/languages.py @@ -7,6 +7,5 @@ class ProgrammingLanguage(SqlAlchemyBase): __tablename__ = 'languages' id: str = sqlalchemy.Column(sqlalchemy.String, primary_key=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) description: str = sqlalchemy.Column(sqlalchemy.String) diff --git a/app/ch10_using_sqlachemy/final/pypi_org/data/package.py b/app/ch10_using_sqlachemy/final/pypi_org/data/package.py index 9e0dcecf..e0e48878 100644 --- a/app/ch10_using_sqlachemy/final/pypi_org/data/package.py +++ b/app/ch10_using_sqlachemy/final/pypi_org/data/package.py @@ -3,6 +3,8 @@ import sqlalchemy as sa import sqlalchemy.orm as orm +from sqlalchemy.orm import Mapped + from pypi_org.data.modelbase import SqlAlchemyBase from pypi_org.data.releases import Release @@ -25,11 +27,15 @@ class Package(SqlAlchemyBase): license: str = sa.Column(sa.String, index=True) # releases relationship - releases: List[Release] = orm.relation("Release", order_by=[ - Release.major_ver.desc(), - Release.minor_ver.desc(), - Release.build_ver.desc(), - ], back_populates='package') + releases: Mapped[Release] = orm.relationship( + 'Release', + order_by=[ + Release.major_ver.desc(), + Release.minor_ver.desc(), + Release.build_ver.desc(), + ], + back_populates='package', + ) def __repr__(self): return ''.format(self.id) diff --git a/app/ch10_using_sqlachemy/final/pypi_org/data/releases.py b/app/ch10_using_sqlachemy/final/pypi_org/data/releases.py index 7650e438..4b2db2bf 100644 --- a/app/ch10_using_sqlachemy/final/pypi_org/data/releases.py +++ b/app/ch10_using_sqlachemy/final/pypi_org/data/releases.py @@ -13,16 +13,14 @@ class Release(SqlAlchemyBase): minor_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) build_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) - created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, - default=datetime.datetime.now, - index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) comment: str = sqlalchemy.Column(sqlalchemy.String) url: str = sqlalchemy.Column(sqlalchemy.String) size: int = sqlalchemy.Column(sqlalchemy.BigInteger) # Package relationship - package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey("packages.id")) - package = orm.relation('Package') + package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey('packages.id')) + package = orm.relationship('Package') @property def version_text(self): diff --git a/app/ch10_using_sqlachemy/final/pypi_org/infrastructure/num_convert.py b/app/ch10_using_sqlachemy/final/pypi_org/infrastructure/num_convert.py new file mode 100644 index 00000000..4e1d8879 --- /dev/null +++ b/app/ch10_using_sqlachemy/final/pypi_org/infrastructure/num_convert.py @@ -0,0 +1,8 @@ +from typing import Optional + + +def try_int(text) -> Optional[int]: + try: + return int(text) + except: + return None diff --git a/app/ch10_using_sqlachemy/final/pypi_org/infrastructure/view_modifiers.py b/app/ch10_using_sqlachemy/final/pypi_org/infrastructure/view_modifiers.py index 5bb6c178..d944c142 100644 --- a/app/ch10_using_sqlachemy/final/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch10_using_sqlachemy/final/pypi_org/infrastructure/view_modifiers.py @@ -1,6 +1,8 @@ from functools import wraps import flask +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -10,6 +12,10 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) + + if isinstance(response_val, werkzeug.wrappers.Response): + return response_val + if isinstance(response_val, flask.Response): return response_val @@ -20,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -36,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch10_using_sqlachemy/final/pypi_org/services/package_service.py b/app/ch10_using_sqlachemy/final/pypi_org/services/package_service.py index e51241ae..6ac445e6 100644 --- a/app/ch10_using_sqlachemy/final/pypi_org/services/package_service.py +++ b/app/ch10_using_sqlachemy/final/pypi_org/services/package_service.py @@ -9,11 +9,13 @@ def get_latest_releases(limit=10) -> List[Release]: session = db_session.create_session() - releases = session.query(Release). \ - options(sqlalchemy.orm.joinedload(Release.package)). \ - order_by(Release.created_date.desc()). \ - limit(limit). \ - all() + releases = ( + session.query(Release) + .options(sqlalchemy.orm.joinedload(Release.package)) + .order_by(Release.created_date.desc()) + .limit(limit) + .all() + ) session.close() @@ -38,10 +40,12 @@ def get_package_by_id(package_id: str) -> Optional[Package]: session = db_session.create_session() - package = session.query(Package) \ - .options(sqlalchemy.orm.joinedload(Package.releases)) \ - .filter(Package.id == package_id) \ + package = ( + session.query(Package) + .options(sqlalchemy.orm.joinedload(Package.releases)) + .filter(Package.id == package_id) .first() + ) session.close() diff --git a/app/ch10_using_sqlachemy/final/pypi_org/views/account_views.py b/app/ch10_using_sqlachemy/final/pypi_org/views/account_views.py index 0421324a..55e5df41 100644 --- a/app/ch10_using_sqlachemy/final/pypi_org/views/account_views.py +++ b/app/ch10_using_sqlachemy/final/pypi_org/views/account_views.py @@ -16,6 +16,7 @@ def index(): # ################### REGISTER ################################# + @blueprint.route('/account/register', methods=['GET']) @response(template_file='account/register.html') def register_get(): @@ -30,6 +31,7 @@ def register_post(): # ################### LOGIN ################################# + @blueprint.route('/account/login', methods=['GET']) @response(template_file='account/login.html') def login_get(): @@ -44,6 +46,7 @@ def login_post(): # ################### LOGOUT ################################# + @blueprint.route('/account/logout') def logout(): return {} diff --git a/app/ch10_using_sqlachemy/final/pypi_org/views/cms_views.py b/app/ch10_using_sqlachemy/final/pypi_org/views/cms_views.py index f7172d3d..a8b755a8 100644 --- a/app/ch10_using_sqlachemy/final/pypi_org/views/cms_views.py +++ b/app/ch10_using_sqlachemy/final/pypi_org/views/cms_views.py @@ -9,7 +9,7 @@ @blueprint.route('/') @response(template_file='cms/page.html') def cms_page(full_url: str): - print("Getting CMS page for {}".format(full_url)) + print('Getting CMS page for {}'.format(full_url)) page = cms_service.get_page(full_url) if not page: diff --git a/app/ch10_using_sqlachemy/final/pypi_org/views/package_views.py b/app/ch10_using_sqlachemy/final/pypi_org/views/package_views.py index 70526f59..4589fe56 100644 --- a/app/ch10_using_sqlachemy/final/pypi_org/views/package_views.py +++ b/app/ch10_using_sqlachemy/final/pypi_org/views/package_views.py @@ -16,12 +16,12 @@ def package_details(package_name: str): if not package: return flask.abort(status=404) - latest_version = "0.0.0" + latest_version = '0.0.0' latest_release = None is_latest = True if package.releases: - latest_release = package.releases[0] + latest_release = package.releases latest_version = latest_release.version_text return { @@ -36,4 +36,4 @@ def package_details(package_name: str): @blueprint.route('/') def popular(rank: int): print(type(rank), rank) - return "The details for the {}th most popular package".format(rank) + return 'The details for the {}th most popular package'.format(rank) diff --git a/app/ch10_using_sqlachemy/final/requirements.piptools b/app/ch10_using_sqlachemy/final/requirements.piptools new file mode 100644 index 00000000..2fbbdb71 --- /dev/null +++ b/app/ch10_using_sqlachemy/final/requirements.piptools @@ -0,0 +1,4 @@ +flask +progressbar2 +python-dateutil +sqlalchemy diff --git a/app/ch10_using_sqlachemy/final/requirements.txt b/app/ch10_using_sqlachemy/final/requirements.txt index 9cf703d9..943a0812 100644 --- a/app/ch10_using_sqlachemy/final/requirements.txt +++ b/app/ch10_using_sqlachemy/final/requirements.txt @@ -1,6 +1,32 @@ -flask -sqlalchemy - -progressbar2 -python-dateutil - +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +markupsafe==3.0.2 + # via + # jinja2 + # werkzeug +progressbar2==4.5.0 + # via -r requirements.piptools +python-dateutil==2.9.0.post0 + # via -r requirements.piptools +python-utils==3.9.1 + # via progressbar2 +six==1.17.0 + # via python-dateutil +sqlalchemy==2.0.38 + # via -r requirements.piptools +typing-extensions==4.12.2 + # via + # python-utils + # sqlalchemy +werkzeug==3.1.3 + # via flask diff --git a/app/ch10_using_sqlachemy/starter/.idea/codeStyles/codeStyleConfig.xml b/app/ch10_using_sqlachemy/starter/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..a55e7a17 --- /dev/null +++ b/app/ch10_using_sqlachemy/starter/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/ch10_using_sqlachemy/starter/.idea/flask ch10_using_sqlachemy - starter.iml b/app/ch10_using_sqlachemy/starter/.idea/flask ch10_using_sqlachemy - starter.iml new file mode 100644 index 00000000..f50d93bf --- /dev/null +++ b/app/ch10_using_sqlachemy/starter/.idea/flask ch10_using_sqlachemy - starter.iml @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/ch10_using_sqlachemy/starter/pypi_org/app.py b/app/ch10_using_sqlachemy/starter/pypi_org/app.py index 4b39b374..0315d74a 100644 --- a/app/ch10_using_sqlachemy/starter/pypi_org/app.py +++ b/app/ch10_using_sqlachemy/starter/pypi_org/app.py @@ -1,6 +1,7 @@ import os import sys import flask + folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, folder) @@ -16,10 +17,7 @@ def main(): def setup_db(): - db_file = os.path.join( - os.path.dirname(__file__), - 'db', - 'pypi.sqlite') + db_file = os.path.join(os.path.dirname(__file__), 'db', 'pypi.sqlite') db_session.global_init(db_file) diff --git a/app/ch10_using_sqlachemy/starter/pypi_org/data/__all_models.py b/app/ch10_using_sqlachemy/starter/pypi_org/data/__all_models.py index 6c137449..1b24d092 100644 --- a/app/ch10_using_sqlachemy/starter/pypi_org/data/__all_models.py +++ b/app/ch10_using_sqlachemy/starter/pypi_org/data/__all_models.py @@ -5,15 +5,21 @@ # noinspection PyUnresolvedReferences import pypi_org.data.downloads + # noinspection PyUnresolvedReferences import pypi_org.data.languages + # noinspection PyUnresolvedReferences import pypi_org.data.licenses + # noinspection PyUnresolvedReferences import pypi_org.data.maintainers + # noinspection PyUnresolvedReferences import pypi_org.data.package + # noinspection PyUnresolvedReferences import pypi_org.data.releases + # noinspection PyUnresolvedReferences import pypi_org.data.users diff --git a/app/ch10_using_sqlachemy/starter/pypi_org/data/db_session.py b/app/ch10_using_sqlachemy/starter/pypi_org/data/db_session.py index 24973d64..4a39c10f 100644 --- a/app/ch10_using_sqlachemy/starter/pypi_org/data/db_session.py +++ b/app/ch10_using_sqlachemy/starter/pypi_org/data/db_session.py @@ -13,12 +13,15 @@ def global_init(db_file: str): return if not db_file or not db_file.strip(): - raise Exception("You must specify a db file.") + raise Exception('You must specify a db file.') conn_str = 'sqlite:///' + db_file.strip() - print("Connecting to DB with {}".format(conn_str)) + print('Connecting to DB with {}'.format(conn_str)) - engine = sa.create_engine(conn_str, echo=False) + # Adding check_same_thread = False after the recording. This can be an issue about + # creating / owner thread when cleaning up sessions, etc. This is a sqlite restriction + # that we probably don't care about in this example. + engine = sa.create_engine(conn_str, echo=False, connect_args={'check_same_thread': False}) factory = orm.sessionmaker(bind=engine) # noinspection PyUnresolvedReferences diff --git a/app/ch10_using_sqlachemy/starter/pypi_org/data/package.py b/app/ch10_using_sqlachemy/starter/pypi_org/data/package.py index d3806ea3..e0e48878 100644 --- a/app/ch10_using_sqlachemy/starter/pypi_org/data/package.py +++ b/app/ch10_using_sqlachemy/starter/pypi_org/data/package.py @@ -3,6 +3,8 @@ import sqlalchemy as sa import sqlalchemy.orm as orm +from sqlalchemy.orm import Mapped + from pypi_org.data.modelbase import SqlAlchemyBase from pypi_org.data.releases import Release @@ -10,26 +12,30 @@ class Package(SqlAlchemyBase): __tablename__ = 'packages' - id = sa.Column(sa.String, primary_key=True) - created_date = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) - summary = sa.Column(sa.String, nullable=False) - description = sa.Column(sa.String, nullable=True) + id: str = sa.Column(sa.String, primary_key=True) + created_date: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) + summary: str = sa.Column(sa.String, nullable=False) + description: str = sa.Column(sa.String, nullable=True) - home_page = sa.Column(sa.String) - docs_url = sa.Column(sa.String) - package_url = sa.Column(sa.String) + home_page: str = sa.Column(sa.String) + docs_url: str = sa.Column(sa.String) + package_url: str = sa.Column(sa.String) - author_name = sa.Column(sa.String) - author_email = sa.Column(sa.String, index=True) + author_name: str = sa.Column(sa.String) + author_email: str = sa.Column(sa.String, index=True) - license = sa.Column(sa.String, index=True) + license: str = sa.Column(sa.String, index=True) # releases relationship - releases: List[Release] = orm.relation("Release", order_by=[ - Release.major_ver.desc(), - Release.minor_ver.desc(), - Release.build_ver.desc(), - ], back_populates='package') + releases: Mapped[Release] = orm.relationship( + 'Release', + order_by=[ + Release.major_ver.desc(), + Release.minor_ver.desc(), + Release.build_ver.desc(), + ], + back_populates='package', + ) def __repr__(self): return ''.format(self.id) diff --git a/app/ch10_using_sqlachemy/starter/pypi_org/data/releases.py b/app/ch10_using_sqlachemy/starter/pypi_org/data/releases.py index 682fa08a..4b2db2bf 100644 --- a/app/ch10_using_sqlachemy/starter/pypi_org/data/releases.py +++ b/app/ch10_using_sqlachemy/starter/pypi_org/data/releases.py @@ -7,23 +7,21 @@ class Release(SqlAlchemyBase): __tablename__ = 'releases' - id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True, autoincrement=True) + id: int = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True, autoincrement=True) - major_ver = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) - minor_ver = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) - build_ver = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) + major_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) + minor_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) + build_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) - created_date = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) - comment = sqlalchemy.Column(sqlalchemy.String) - url = sqlalchemy.Column(sqlalchemy.String) - size = sqlalchemy.Column(sqlalchemy.BigInteger) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + comment: str = sqlalchemy.Column(sqlalchemy.String) + url: str = sqlalchemy.Column(sqlalchemy.String) + size: int = sqlalchemy.Column(sqlalchemy.BigInteger) # Package relationship - package_id = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey("packages.id")) - package = orm.relation('Package') + package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey('packages.id')) + package = orm.relationship('Package') @property def version_text(self): return '{}.{}.{}'.format(self.major_ver, self.minor_ver, self.build_ver) - - diff --git a/app/ch10_using_sqlachemy/starter/pypi_org/infrastructure/num_convert.py b/app/ch10_using_sqlachemy/starter/pypi_org/infrastructure/num_convert.py new file mode 100644 index 00000000..4e1d8879 --- /dev/null +++ b/app/ch10_using_sqlachemy/starter/pypi_org/infrastructure/num_convert.py @@ -0,0 +1,8 @@ +from typing import Optional + + +def try_int(text) -> Optional[int]: + try: + return int(text) + except: + return None diff --git a/app/ch10_using_sqlachemy/starter/pypi_org/infrastructure/view_modifiers.py b/app/ch10_using_sqlachemy/starter/pypi_org/infrastructure/view_modifiers.py index 5bb6c178..d944c142 100644 --- a/app/ch10_using_sqlachemy/starter/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch10_using_sqlachemy/starter/pypi_org/infrastructure/view_modifiers.py @@ -1,6 +1,8 @@ from functools import wraps import flask +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -10,6 +12,10 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) + + if isinstance(response_val, werkzeug.wrappers.Response): + return response_val + if isinstance(response_val, flask.Response): return response_val @@ -20,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -36,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch10_using_sqlachemy/starter/pypi_org/views/account_views.py b/app/ch10_using_sqlachemy/starter/pypi_org/views/account_views.py index 0421324a..55e5df41 100644 --- a/app/ch10_using_sqlachemy/starter/pypi_org/views/account_views.py +++ b/app/ch10_using_sqlachemy/starter/pypi_org/views/account_views.py @@ -16,6 +16,7 @@ def index(): # ################### REGISTER ################################# + @blueprint.route('/account/register', methods=['GET']) @response(template_file='account/register.html') def register_get(): @@ -30,6 +31,7 @@ def register_post(): # ################### LOGIN ################################# + @blueprint.route('/account/login', methods=['GET']) @response(template_file='account/login.html') def login_get(): @@ -44,6 +46,7 @@ def login_post(): # ################### LOGOUT ################################# + @blueprint.route('/account/logout') def logout(): return {} diff --git a/app/ch10_using_sqlachemy/starter/pypi_org/views/cms_views.py b/app/ch10_using_sqlachemy/starter/pypi_org/views/cms_views.py index f7172d3d..a8b755a8 100644 --- a/app/ch10_using_sqlachemy/starter/pypi_org/views/cms_views.py +++ b/app/ch10_using_sqlachemy/starter/pypi_org/views/cms_views.py @@ -9,7 +9,7 @@ @blueprint.route('/') @response(template_file='cms/page.html') def cms_page(full_url: str): - print("Getting CMS page for {}".format(full_url)) + print('Getting CMS page for {}'.format(full_url)) page = cms_service.get_page(full_url) if not page: diff --git a/app/ch10_using_sqlachemy/starter/pypi_org/views/package_views.py b/app/ch10_using_sqlachemy/starter/pypi_org/views/package_views.py index ccf6bf12..7450fa1c 100644 --- a/app/ch10_using_sqlachemy/starter/pypi_org/views/package_views.py +++ b/app/ch10_using_sqlachemy/starter/pypi_org/views/package_views.py @@ -9,10 +9,10 @@ @blueprint.route('/project/') # @response(template_file='packages/details.html') def package_details(package_name: str): - return "Package details for {}".format(package_name) + return 'Package details for {}'.format(package_name) @blueprint.route('/') def popular(rank: int): print(type(rank), rank) - return "The details for the {}th most popular package".format(rank) + return 'The details for the {}th most popular package'.format(rank) diff --git a/app/ch10_using_sqlachemy/starter/requirements.piptools b/app/ch10_using_sqlachemy/starter/requirements.piptools new file mode 100644 index 00000000..e39cea14 --- /dev/null +++ b/app/ch10_using_sqlachemy/starter/requirements.piptools @@ -0,0 +1,2 @@ +flask +sqlalchemy diff --git a/app/ch10_using_sqlachemy/starter/requirements.txt b/app/ch10_using_sqlachemy/starter/requirements.txt index e39cea14..25908d2b 100644 --- a/app/ch10_using_sqlachemy/starter/requirements.txt +++ b/app/ch10_using_sqlachemy/starter/requirements.txt @@ -1,2 +1,22 @@ -flask -sqlalchemy +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +markupsafe==3.0.2 + # via + # jinja2 + # werkzeug +sqlalchemy==2.0.38 + # via -r requirements.piptools +typing-extensions==4.12.2 + # via sqlalchemy +werkzeug==3.1.3 + # via flask diff --git a/app/ch11_migrations/final/.idea/.name b/app/ch11_migrations/final/.idea/.name index e0d1ea3b..96b7dad3 100644 --- a/app/ch11_migrations/final/.idea/.name +++ b/app/ch11_migrations/final/.idea/.name @@ -1 +1 @@ -migrations \ No newline at end of file +flask ch11_migrations - final \ No newline at end of file diff --git a/app/ch10_using_sqlachemy/final/.idea/using_sqlachemy.iml b/app/ch11_migrations/final/.idea/flask ch11_migrations - final.iml similarity index 100% rename from app/ch10_using_sqlachemy/final/.idea/using_sqlachemy.iml rename to app/ch11_migrations/final/.idea/flask ch11_migrations - final.iml diff --git a/app/ch11_migrations/final/.idea/inspectionProfiles/Project_Default.xml b/app/ch11_migrations/final/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..05d4a622 --- /dev/null +++ b/app/ch11_migrations/final/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,21 @@ + + + + \ No newline at end of file diff --git a/app/ch11_migrations/final/.idea/modules.xml b/app/ch11_migrations/final/.idea/modules.xml index c46f156d..9b276c4f 100644 --- a/app/ch11_migrations/final/.idea/modules.xml +++ b/app/ch11_migrations/final/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/app/ch11_migrations/final/alembic/alembic_helpers.py b/app/ch11_migrations/final/alembic/alembic_helpers.py index 561f7a70..6aea52d4 100644 --- a/app/ch11_migrations/final/alembic/alembic_helpers.py +++ b/app/ch11_migrations/final/alembic/alembic_helpers.py @@ -5,8 +5,7 @@ def table_has_column(table, column): config = op.get_context().config - engine = engine_from_config( - config.get_section(config.config_ini_section), prefix='sqlalchemy.') + engine = engine_from_config(config.get_section(config.config_ini_section), prefix='sqlalchemy.') insp = reflection.Inspector.from_engine(engine) has_column = False for col in insp.get_columns(table): diff --git a/app/ch11_migrations/final/alembic/env.py b/app/ch11_migrations/final/alembic/env.py index 27ec49fb..1fdc9cab 100644 --- a/app/ch11_migrations/final/alembic/env.py +++ b/app/ch11_migrations/final/alembic/env.py @@ -26,6 +26,7 @@ sys.path.insert(0, folder) from pypi_org.data.modelbase import SqlAlchemyBase + # noinspection PyUnresolvedReferences import pypi_org.data.__all_models @@ -49,10 +50,8 @@ def run_migrations_offline(): script output. """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, target_metadata=target_metadata, literal_binds=True - ) + url = config.get_main_option('sqlalchemy.url') + context.configure(url=url, target_metadata=target_metadata, literal_binds=True) with context.begin_transaction(): context.run_migrations() @@ -67,14 +66,12 @@ def run_migrations_online(): """ connectable = engine_from_config( config.get_section(config.config_ini_section), - prefix="sqlalchemy.", + prefix='sqlalchemy.', poolclass=pool.NullPool, ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() diff --git a/app/ch11_migrations/final/alembic/versions/722c82f0097c_added_auditing_table.py b/app/ch11_migrations/final/alembic/versions/722c82f0097c_added_auditing_table.py index 2c3e2a39..7c6e892f 100644 --- a/app/ch11_migrations/final/alembic/versions/722c82f0097c_added_auditing_table.py +++ b/app/ch11_migrations/final/alembic/versions/722c82f0097c_added_auditing_table.py @@ -18,11 +18,12 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('auditing', - sa.Column('id', sa.String(), nullable=False), - sa.Column('created_date', sa.DateTime(), nullable=True), - sa.Column('description', sa.String(), nullable=True), - sa.PrimaryKeyConstraint('id') + op.create_table( + 'auditing', + sa.Column('id', sa.String(), nullable=False), + sa.Column('created_date', sa.DateTime(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id'), ) op.create_index(op.f('ix_auditing_created_date'), 'auditing', ['created_date'], unique=False) # ### end Alembic commands ### diff --git a/app/ch11_migrations/final/pypi_org/app.py b/app/ch11_migrations/final/pypi_org/app.py index 4b39b374..0315d74a 100644 --- a/app/ch11_migrations/final/pypi_org/app.py +++ b/app/ch11_migrations/final/pypi_org/app.py @@ -1,6 +1,7 @@ import os import sys import flask + folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, folder) @@ -16,10 +17,7 @@ def main(): def setup_db(): - db_file = os.path.join( - os.path.dirname(__file__), - 'db', - 'pypi.sqlite') + db_file = os.path.join(os.path.dirname(__file__), 'db', 'pypi.sqlite') db_session.global_init(db_file) diff --git a/app/ch11_migrations/final/pypi_org/bin/basic_inserts.py b/app/ch11_migrations/final/pypi_org/bin/basic_inserts.py index 3a2f360d..6d04f551 100644 --- a/app/ch11_migrations/final/pypi_org/bin/basic_inserts.py +++ b/app/ch11_migrations/final/pypi_org/bin/basic_inserts.py @@ -1,14 +1,13 @@ import os import sys +# Make it run more easily outside of PyCharm +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + import pypi_org.data.db_session as db_session from pypi_org.data.package import Package from pypi_org.data.releases import Release -# Make it run more easily outside of PyCharm -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) - def main(): init_db() @@ -20,24 +19,24 @@ def insert_a_package(): p = Package() p.id = input('Package id / name: ').strip().lower() - p.summary = input("Package summary: ").strip() - p.author_name = input("Author: ").strip() - p.license = input("License: ").strip() + p.summary = input('Package summary: ').strip() + p.author_name = input('Author: ').strip() + p.license = input('License: ').strip() - print("Release 1:") + print('Release 1:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) - print("Release 2:") + print('Release 2:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) session = db_session.create_session() diff --git a/app/ch11_migrations/final/pypi_org/bin/load_data.py b/app/ch11_migrations/final/pypi_org/bin/load_data.py index 15237f18..6e0bd617 100644 --- a/app/ch11_migrations/final/pypi_org/bin/load_data.py +++ b/app/ch11_migrations/final/pypi_org/bin/load_data.py @@ -7,8 +7,9 @@ import progressbar from dateutil.parser import parse -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) +from pypi_org.infrastructure.num_convert import try_int + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) import pypi_org.data.db_session as db_session from pypi_org.data.languages import ProgrammingLanguage @@ -39,7 +40,7 @@ def main(): def do_import_languages(file_data: List[dict]): imported = set() - print("Importing languages ... ", flush=True) + print('Importing languages ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -74,7 +75,7 @@ def do_import_languages(file_data: List[dict]): def do_import_licenses(file_data: List[dict]): imported = set() - print("Importing licenses ... ", flush=True) + print('Importing licenses ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -100,17 +101,17 @@ def do_import_licenses(file_data: List[dict]): def do_summary(): session = db_session.create_session() - print("Final numbers:") - print("Users: {:,}".format(session.query(User).count())) - print("Packages: {:,}".format(session.query(Package).count())) - print("Releases: {:,}".format(session.query(Release).count())) - print("Maintainers: {:,}".format(session.query(Maintainer).count())) - print("Languages: {:,}".format(session.query(ProgrammingLanguage).count())) - print("Licenses: {:,}".format(session.query(License).count())) + print('Final numbers:') + print('Users: {:,}'.format(session.query(User).count())) + print('Packages: {:,}'.format(session.query(Package).count())) + print('Releases: {:,}'.format(session.query(Release).count())) + print('Maintainers: {:,}'.format(session.query(Maintainer).count())) + print('Languages: {:,}'.format(session.query(ProgrammingLanguage).count())) + print('Licenses: {:,}'.format(session.query(License).count())) def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: - print("Importing users ... ", flush=True) + print('Importing users ... ', flush=True) with progressbar.ProgressBar(max_value=len(user_lookup)) as bar: for idx, (email, name) in enumerate(user_lookup.items()): session = db_session.create_session() @@ -134,29 +135,29 @@ def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: def do_import_packages(file_data: List[dict], user_lookup: Dict[str, User]): errored_packages = [] - print("Importing packages and releases ... ", flush=True) + print('Importing packages and releases ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): try: load_package(p, user_lookup) bar.update(idx) except Exception as x: - errored_packages.append((p, " *** Errored out for package {}, {}".format(p.get('package_name'), x))) + errored_packages.append((p, ' *** Errored out for package {}, {}'.format(p.get('package_name'), x))) raise sys.stderr.flush() sys.stdout.flush() print() - print("Completed packages with {} errors.".format(len(errored_packages))) - for (p, txt) in errored_packages: + print('Completed packages with {} errors.'.format(len(errored_packages))) + for p, txt in errored_packages: print(txt) def do_load_files() -> List[dict]: data_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../../data/pypi-top-100')) - print("Loading files from {}".format(data_path)) + print('Loading files from {}'.format(data_path)) files = get_file_names(data_path) - print("Found {:,} files, loading ...".format(len(files)), flush=True) - time.sleep(.1) + print('Found {:,} files, loading ...'.format(len(files)), flush=True) + time.sleep(0.1) file_data = [] with progressbar.ProgressBar(max_value=len(files)) as bar: @@ -171,7 +172,7 @@ def do_load_files() -> List[dict]: def find_users(data: List[dict]) -> dict: - print("Discovering users...", flush=True) + print('Discovering users...', flush=True) found_users = {} with progressbar.ProgressBar(max_value=len(data)) as bar: @@ -184,7 +185,7 @@ def find_users(data: List[dict]) -> dict: sys.stderr.flush() sys.stdout.flush() print() - print("Discovered {:,} users".format(len(found_users))) + print('Discovered {:,} users'.format(len(found_users))) print() return found_users @@ -215,7 +216,7 @@ def load_file_data(filename: str) -> dict: with open(filename, 'r', encoding='utf-8') as fin: data = json.load(fin) except Exception as x: - print("ERROR in file: {}, details: {}".format(filename, x), flush=True) + print('ERROR in file: {}, details: {}'.format(filename, x), flush=True) raise return data @@ -233,7 +234,7 @@ def load_package(data: dict, user_lookup: Dict[str, User]): p.author = info.get('author') p.author_email = info.get('author_email') - releases = build_releases(p.id, data.get("releases", {})) + releases = build_releases(p.id, data.get('releases', {})) if releases: p.created_date = releases[0].created_date @@ -283,18 +284,13 @@ def detect_license(license_text: str) -> Optional[str]: license_text = license_text.strip() if len(license_text) > 100 or '\n' in license_text: - return "CUSTOM" + return 'CUSTOM' - license_text = license_text \ - .replace('Software License', '') \ - .replace('License', '') + license_text = license_text.replace('Software License', '').replace('License', '') if '::' in license_text: # E.g. 'License :: OSI Approved :: Apache Software License' - return license_text \ - .split(':')[-1] \ - .replace(' ', ' ') \ - .strip() + return license_text.split(':')[-1].replace(' ', ' ').strip() return license_text.strip() @@ -339,13 +335,6 @@ def make_version_num(version_text): return major, minor, build -def try_int(text) -> int: - try: - return int(text) - except: - return 0 - - def init_db(): top_folder = os.path.dirname(__file__) rel_file = os.path.join('..', 'db', 'pypi.sqlite') @@ -357,9 +346,7 @@ def get_file_names(data_path: str) -> List[str]: files = [] for f in os.listdir(data_path): if f.endswith('.json'): - files.append( - os.path.abspath(os.path.join(data_path, f)) - ) + files.append(os.path.abspath(os.path.join(data_path, f))) files.sort() return files diff --git a/app/ch11_migrations/final/pypi_org/data/__all_models.py b/app/ch11_migrations/final/pypi_org/data/__all_models.py index f399ef54..ca4804ac 100644 --- a/app/ch11_migrations/final/pypi_org/data/__all_models.py +++ b/app/ch11_migrations/final/pypi_org/data/__all_models.py @@ -5,17 +5,24 @@ # noinspection PyUnresolvedReferences import pypi_org.data.audit + # noinspection PyUnresolvedReferences import pypi_org.data.downloads + # noinspection PyUnresolvedReferences import pypi_org.data.languages + # noinspection PyUnresolvedReferences import pypi_org.data.licenses + # noinspection PyUnresolvedReferences import pypi_org.data.maintainers + # noinspection PyUnresolvedReferences import pypi_org.data.package + # noinspection PyUnresolvedReferences import pypi_org.data.releases + # noinspection PyUnresolvedReferences import pypi_org.data.users diff --git a/app/ch11_migrations/final/pypi_org/data/db_session.py b/app/ch11_migrations/final/pypi_org/data/db_session.py index 50861b3b..62391b85 100644 --- a/app/ch11_migrations/final/pypi_org/data/db_session.py +++ b/app/ch11_migrations/final/pypi_org/data/db_session.py @@ -14,12 +14,15 @@ def global_init(db_file: str): return if not db_file or not db_file.strip(): - raise Exception("You must specify a db file.") + raise Exception('You must specify a db file.') conn_str = 'sqlite:///' + db_file.strip() - print("Connecting to DB with {}".format(conn_str)) + print('Connecting to DB with {}'.format(conn_str)) - engine = sa.create_engine(conn_str, echo=False) + # Adding check_same_thread = False after the recording. This can be an issue about + # creating / owner thread when cleaning up sessions, etc. This is a sqlite restriction + # that we probably don't care about in this example. + engine = sa.create_engine(conn_str, echo=False, connect_args={'check_same_thread': False}) __factory = orm.sessionmaker(bind=engine) # noinspection PyUnresolvedReferences diff --git a/app/ch11_migrations/final/pypi_org/data/downloads.py b/app/ch11_migrations/final/pypi_org/data/downloads.py index 66068f02..dd8afd65 100644 --- a/app/ch11_migrations/final/pypi_org/data/downloads.py +++ b/app/ch11_migrations/final/pypi_org/data/downloads.py @@ -7,8 +7,7 @@ class Download(SqlAlchemyBase): __tablename__ = 'downloads' id: int = sqlalchemy.Column(sqlalchemy.BigInteger, primary_key=True, autoincrement=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) package_id: str = sqlalchemy.Column(sqlalchemy.String, index=True) release_id: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) diff --git a/app/ch11_migrations/final/pypi_org/data/languages.py b/app/ch11_migrations/final/pypi_org/data/languages.py index 964b8444..3a73dde0 100644 --- a/app/ch11_migrations/final/pypi_org/data/languages.py +++ b/app/ch11_migrations/final/pypi_org/data/languages.py @@ -7,6 +7,5 @@ class ProgrammingLanguage(SqlAlchemyBase): __tablename__ = 'languages' id: str = sqlalchemy.Column(sqlalchemy.String, primary_key=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) description: str = sqlalchemy.Column(sqlalchemy.String) diff --git a/app/ch11_migrations/final/pypi_org/data/package.py b/app/ch11_migrations/final/pypi_org/data/package.py index c3420d1b..e0e48878 100644 --- a/app/ch11_migrations/final/pypi_org/data/package.py +++ b/app/ch11_migrations/final/pypi_org/data/package.py @@ -3,6 +3,8 @@ import sqlalchemy as sa import sqlalchemy.orm as orm +from sqlalchemy.orm import Mapped + from pypi_org.data.modelbase import SqlAlchemyBase from pypi_org.data.releases import Release @@ -12,7 +14,6 @@ class Package(SqlAlchemyBase): id: str = sa.Column(sa.String, primary_key=True) created_date: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) - last_updated: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) summary: str = sa.Column(sa.String, nullable=False) description: str = sa.Column(sa.String, nullable=True) @@ -26,11 +27,15 @@ class Package(SqlAlchemyBase): license: str = sa.Column(sa.String, index=True) # releases relationship - releases: List[Release] = orm.relation("Release", order_by=[ - Release.major_ver.desc(), - Release.minor_ver.desc(), - Release.build_ver.desc(), - ], back_populates='package') + releases: Mapped[Release] = orm.relationship( + 'Release', + order_by=[ + Release.major_ver.desc(), + Release.minor_ver.desc(), + Release.build_ver.desc(), + ], + back_populates='package', + ) def __repr__(self): return ''.format(self.id) diff --git a/app/ch11_migrations/final/pypi_org/data/releases.py b/app/ch11_migrations/final/pypi_org/data/releases.py index 7650e438..4b2db2bf 100644 --- a/app/ch11_migrations/final/pypi_org/data/releases.py +++ b/app/ch11_migrations/final/pypi_org/data/releases.py @@ -13,16 +13,14 @@ class Release(SqlAlchemyBase): minor_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) build_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) - created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, - default=datetime.datetime.now, - index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) comment: str = sqlalchemy.Column(sqlalchemy.String) url: str = sqlalchemy.Column(sqlalchemy.String) size: int = sqlalchemy.Column(sqlalchemy.BigInteger) # Package relationship - package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey("packages.id")) - package = orm.relation('Package') + package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey('packages.id')) + package = orm.relationship('Package') @property def version_text(self): diff --git a/app/ch11_migrations/final/pypi_org/infrastructure/num_convert.py b/app/ch11_migrations/final/pypi_org/infrastructure/num_convert.py new file mode 100644 index 00000000..4e1d8879 --- /dev/null +++ b/app/ch11_migrations/final/pypi_org/infrastructure/num_convert.py @@ -0,0 +1,8 @@ +from typing import Optional + + +def try_int(text) -> Optional[int]: + try: + return int(text) + except: + return None diff --git a/app/ch11_migrations/final/pypi_org/infrastructure/view_modifiers.py b/app/ch11_migrations/final/pypi_org/infrastructure/view_modifiers.py index 5bb6c178..d944c142 100644 --- a/app/ch11_migrations/final/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch11_migrations/final/pypi_org/infrastructure/view_modifiers.py @@ -1,6 +1,8 @@ from functools import wraps import flask +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -10,6 +12,10 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) + + if isinstance(response_val, werkzeug.wrappers.Response): + return response_val + if isinstance(response_val, flask.Response): return response_val @@ -20,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -36,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch11_migrations/final/pypi_org/services/package_service.py b/app/ch11_migrations/final/pypi_org/services/package_service.py index e51241ae..6ac445e6 100644 --- a/app/ch11_migrations/final/pypi_org/services/package_service.py +++ b/app/ch11_migrations/final/pypi_org/services/package_service.py @@ -9,11 +9,13 @@ def get_latest_releases(limit=10) -> List[Release]: session = db_session.create_session() - releases = session.query(Release). \ - options(sqlalchemy.orm.joinedload(Release.package)). \ - order_by(Release.created_date.desc()). \ - limit(limit). \ - all() + releases = ( + session.query(Release) + .options(sqlalchemy.orm.joinedload(Release.package)) + .order_by(Release.created_date.desc()) + .limit(limit) + .all() + ) session.close() @@ -38,10 +40,12 @@ def get_package_by_id(package_id: str) -> Optional[Package]: session = db_session.create_session() - package = session.query(Package) \ - .options(sqlalchemy.orm.joinedload(Package.releases)) \ - .filter(Package.id == package_id) \ + package = ( + session.query(Package) + .options(sqlalchemy.orm.joinedload(Package.releases)) + .filter(Package.id == package_id) .first() + ) session.close() diff --git a/app/ch11_migrations/final/pypi_org/views/account_views.py b/app/ch11_migrations/final/pypi_org/views/account_views.py index 0421324a..55e5df41 100644 --- a/app/ch11_migrations/final/pypi_org/views/account_views.py +++ b/app/ch11_migrations/final/pypi_org/views/account_views.py @@ -16,6 +16,7 @@ def index(): # ################### REGISTER ################################# + @blueprint.route('/account/register', methods=['GET']) @response(template_file='account/register.html') def register_get(): @@ -30,6 +31,7 @@ def register_post(): # ################### LOGIN ################################# + @blueprint.route('/account/login', methods=['GET']) @response(template_file='account/login.html') def login_get(): @@ -44,6 +46,7 @@ def login_post(): # ################### LOGOUT ################################# + @blueprint.route('/account/logout') def logout(): return {} diff --git a/app/ch11_migrations/final/pypi_org/views/cms_views.py b/app/ch11_migrations/final/pypi_org/views/cms_views.py index f7172d3d..a8b755a8 100644 --- a/app/ch11_migrations/final/pypi_org/views/cms_views.py +++ b/app/ch11_migrations/final/pypi_org/views/cms_views.py @@ -9,7 +9,7 @@ @blueprint.route('/') @response(template_file='cms/page.html') def cms_page(full_url: str): - print("Getting CMS page for {}".format(full_url)) + print('Getting CMS page for {}'.format(full_url)) page = cms_service.get_page(full_url) if not page: diff --git a/app/ch11_migrations/final/pypi_org/views/package_views.py b/app/ch11_migrations/final/pypi_org/views/package_views.py index 70526f59..4589fe56 100644 --- a/app/ch11_migrations/final/pypi_org/views/package_views.py +++ b/app/ch11_migrations/final/pypi_org/views/package_views.py @@ -16,12 +16,12 @@ def package_details(package_name: str): if not package: return flask.abort(status=404) - latest_version = "0.0.0" + latest_version = '0.0.0' latest_release = None is_latest = True if package.releases: - latest_release = package.releases[0] + latest_release = package.releases latest_version = latest_release.version_text return { @@ -36,4 +36,4 @@ def package_details(package_name: str): @blueprint.route('/') def popular(rank: int): print(type(rank), rank) - return "The details for the {}th most popular package".format(rank) + return 'The details for the {}th most popular package'.format(rank) diff --git a/app/ch11_migrations/final/requirements.piptools b/app/ch11_migrations/final/requirements.piptools new file mode 100644 index 00000000..5ba68752 --- /dev/null +++ b/app/ch11_migrations/final/requirements.piptools @@ -0,0 +1,5 @@ +alembic +flask +progressbar2 +python-dateutil +sqlalchemy diff --git a/app/ch11_migrations/final/requirements.txt b/app/ch11_migrations/final/requirements.txt index 9cf703d9..d802976d 100644 --- a/app/ch11_migrations/final/requirements.txt +++ b/app/ch11_migrations/final/requirements.txt @@ -1,6 +1,40 @@ -flask -sqlalchemy - -progressbar2 -python-dateutil - +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +alembic==1.14.1 + # via -r requirements.piptools +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +mako==1.3.9 + # via alembic +markupsafe==3.0.2 + # via + # jinja2 + # mako + # werkzeug +progressbar2==4.5.0 + # via -r requirements.piptools +python-dateutil==2.9.0.post0 + # via -r requirements.piptools +python-utils==3.9.1 + # via progressbar2 +six==1.17.0 + # via python-dateutil +sqlalchemy==2.0.38 + # via + # -r requirements.piptools + # alembic +typing-extensions==4.12.2 + # via + # alembic + # python-utils + # sqlalchemy +werkzeug==3.1.3 + # via flask diff --git a/app/ch11_migrations/final/.idea/migrations.iml b/app/ch11_migrations/starter/.idea/flask ch11_migrations - starter.iml similarity index 82% rename from app/ch11_migrations/final/.idea/migrations.iml rename to app/ch11_migrations/starter/.idea/flask ch11_migrations - starter.iml index cb75d37a..2fbaf4e6 100644 --- a/app/ch11_migrations/final/.idea/migrations.iml +++ b/app/ch11_migrations/starter/.idea/flask ch11_migrations - starter.iml @@ -13,7 +13,4 @@ - - \ No newline at end of file diff --git a/app/ch11_migrations/starter/pypi_org/app.py b/app/ch11_migrations/starter/pypi_org/app.py index 4b39b374..0315d74a 100644 --- a/app/ch11_migrations/starter/pypi_org/app.py +++ b/app/ch11_migrations/starter/pypi_org/app.py @@ -1,6 +1,7 @@ import os import sys import flask + folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, folder) @@ -16,10 +17,7 @@ def main(): def setup_db(): - db_file = os.path.join( - os.path.dirname(__file__), - 'db', - 'pypi.sqlite') + db_file = os.path.join(os.path.dirname(__file__), 'db', 'pypi.sqlite') db_session.global_init(db_file) diff --git a/app/ch11_migrations/starter/pypi_org/bin/basic_inserts.py b/app/ch11_migrations/starter/pypi_org/bin/basic_inserts.py index 3a2f360d..6d04f551 100644 --- a/app/ch11_migrations/starter/pypi_org/bin/basic_inserts.py +++ b/app/ch11_migrations/starter/pypi_org/bin/basic_inserts.py @@ -1,14 +1,13 @@ import os import sys +# Make it run more easily outside of PyCharm +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + import pypi_org.data.db_session as db_session from pypi_org.data.package import Package from pypi_org.data.releases import Release -# Make it run more easily outside of PyCharm -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) - def main(): init_db() @@ -20,24 +19,24 @@ def insert_a_package(): p = Package() p.id = input('Package id / name: ').strip().lower() - p.summary = input("Package summary: ").strip() - p.author_name = input("Author: ").strip() - p.license = input("License: ").strip() + p.summary = input('Package summary: ').strip() + p.author_name = input('Author: ').strip() + p.license = input('License: ').strip() - print("Release 1:") + print('Release 1:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) - print("Release 2:") + print('Release 2:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) session = db_session.create_session() diff --git a/app/ch11_migrations/starter/pypi_org/bin/load_data.py b/app/ch11_migrations/starter/pypi_org/bin/load_data.py index 15237f18..3af7a03d 100644 --- a/app/ch11_migrations/starter/pypi_org/bin/load_data.py +++ b/app/ch11_migrations/starter/pypi_org/bin/load_data.py @@ -7,9 +7,9 @@ import progressbar from dateutil.parser import parse -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) +from pypi_org.infrastructure.num_convert import try_int import pypi_org.data.db_session as db_session from pypi_org.data.languages import ProgrammingLanguage from pypi_org.data.licenses import License @@ -39,7 +39,7 @@ def main(): def do_import_languages(file_data: List[dict]): imported = set() - print("Importing languages ... ", flush=True) + print('Importing languages ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -74,7 +74,7 @@ def do_import_languages(file_data: List[dict]): def do_import_licenses(file_data: List[dict]): imported = set() - print("Importing licenses ... ", flush=True) + print('Importing licenses ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -100,17 +100,17 @@ def do_import_licenses(file_data: List[dict]): def do_summary(): session = db_session.create_session() - print("Final numbers:") - print("Users: {:,}".format(session.query(User).count())) - print("Packages: {:,}".format(session.query(Package).count())) - print("Releases: {:,}".format(session.query(Release).count())) - print("Maintainers: {:,}".format(session.query(Maintainer).count())) - print("Languages: {:,}".format(session.query(ProgrammingLanguage).count())) - print("Licenses: {:,}".format(session.query(License).count())) + print('Final numbers:') + print('Users: {:,}'.format(session.query(User).count())) + print('Packages: {:,}'.format(session.query(Package).count())) + print('Releases: {:,}'.format(session.query(Release).count())) + print('Maintainers: {:,}'.format(session.query(Maintainer).count())) + print('Languages: {:,}'.format(session.query(ProgrammingLanguage).count())) + print('Licenses: {:,}'.format(session.query(License).count())) def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: - print("Importing users ... ", flush=True) + print('Importing users ... ', flush=True) with progressbar.ProgressBar(max_value=len(user_lookup)) as bar: for idx, (email, name) in enumerate(user_lookup.items()): session = db_session.create_session() @@ -134,29 +134,29 @@ def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: def do_import_packages(file_data: List[dict], user_lookup: Dict[str, User]): errored_packages = [] - print("Importing packages and releases ... ", flush=True) + print('Importing packages and releases ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): try: load_package(p, user_lookup) bar.update(idx) except Exception as x: - errored_packages.append((p, " *** Errored out for package {}, {}".format(p.get('package_name'), x))) + errored_packages.append((p, ' *** Errored out for package {}, {}'.format(p.get('package_name'), x))) raise sys.stderr.flush() sys.stdout.flush() print() - print("Completed packages with {} errors.".format(len(errored_packages))) - for (p, txt) in errored_packages: + print('Completed packages with {} errors.'.format(len(errored_packages))) + for p, txt in errored_packages: print(txt) def do_load_files() -> List[dict]: data_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../../data/pypi-top-100')) - print("Loading files from {}".format(data_path)) + print('Loading files from {}'.format(data_path)) files = get_file_names(data_path) - print("Found {:,} files, loading ...".format(len(files)), flush=True) - time.sleep(.1) + print('Found {:,} files, loading ...'.format(len(files)), flush=True) + time.sleep(0.1) file_data = [] with progressbar.ProgressBar(max_value=len(files)) as bar: @@ -171,7 +171,7 @@ def do_load_files() -> List[dict]: def find_users(data: List[dict]) -> dict: - print("Discovering users...", flush=True) + print('Discovering users...', flush=True) found_users = {} with progressbar.ProgressBar(max_value=len(data)) as bar: @@ -184,7 +184,7 @@ def find_users(data: List[dict]) -> dict: sys.stderr.flush() sys.stdout.flush() print() - print("Discovered {:,} users".format(len(found_users))) + print('Discovered {:,} users'.format(len(found_users))) print() return found_users @@ -215,7 +215,7 @@ def load_file_data(filename: str) -> dict: with open(filename, 'r', encoding='utf-8') as fin: data = json.load(fin) except Exception as x: - print("ERROR in file: {}, details: {}".format(filename, x), flush=True) + print('ERROR in file: {}, details: {}'.format(filename, x), flush=True) raise return data @@ -233,7 +233,7 @@ def load_package(data: dict, user_lookup: Dict[str, User]): p.author = info.get('author') p.author_email = info.get('author_email') - releases = build_releases(p.id, data.get("releases", {})) + releases = build_releases(p.id, data.get('releases', {})) if releases: p.created_date = releases[0].created_date @@ -283,18 +283,13 @@ def detect_license(license_text: str) -> Optional[str]: license_text = license_text.strip() if len(license_text) > 100 or '\n' in license_text: - return "CUSTOM" + return 'CUSTOM' - license_text = license_text \ - .replace('Software License', '') \ - .replace('License', '') + license_text = license_text.replace('Software License', '').replace('License', '') if '::' in license_text: # E.g. 'License :: OSI Approved :: Apache Software License' - return license_text \ - .split(':')[-1] \ - .replace(' ', ' ') \ - .strip() + return license_text.split(':')[-1].replace(' ', ' ').strip() return license_text.strip() @@ -339,13 +334,6 @@ def make_version_num(version_text): return major, minor, build -def try_int(text) -> int: - try: - return int(text) - except: - return 0 - - def init_db(): top_folder = os.path.dirname(__file__) rel_file = os.path.join('..', 'db', 'pypi.sqlite') @@ -357,9 +345,7 @@ def get_file_names(data_path: str) -> List[str]: files = [] for f in os.listdir(data_path): if f.endswith('.json'): - files.append( - os.path.abspath(os.path.join(data_path, f)) - ) + files.append(os.path.abspath(os.path.join(data_path, f))) files.sort() return files diff --git a/app/ch11_migrations/starter/pypi_org/data/__all_models.py b/app/ch11_migrations/starter/pypi_org/data/__all_models.py index 6c137449..1b24d092 100644 --- a/app/ch11_migrations/starter/pypi_org/data/__all_models.py +++ b/app/ch11_migrations/starter/pypi_org/data/__all_models.py @@ -5,15 +5,21 @@ # noinspection PyUnresolvedReferences import pypi_org.data.downloads + # noinspection PyUnresolvedReferences import pypi_org.data.languages + # noinspection PyUnresolvedReferences import pypi_org.data.licenses + # noinspection PyUnresolvedReferences import pypi_org.data.maintainers + # noinspection PyUnresolvedReferences import pypi_org.data.package + # noinspection PyUnresolvedReferences import pypi_org.data.releases + # noinspection PyUnresolvedReferences import pypi_org.data.users diff --git a/app/ch11_migrations/starter/pypi_org/data/db_session.py b/app/ch11_migrations/starter/pypi_org/data/db_session.py index 50861b3b..62391b85 100644 --- a/app/ch11_migrations/starter/pypi_org/data/db_session.py +++ b/app/ch11_migrations/starter/pypi_org/data/db_session.py @@ -14,12 +14,15 @@ def global_init(db_file: str): return if not db_file or not db_file.strip(): - raise Exception("You must specify a db file.") + raise Exception('You must specify a db file.') conn_str = 'sqlite:///' + db_file.strip() - print("Connecting to DB with {}".format(conn_str)) + print('Connecting to DB with {}'.format(conn_str)) - engine = sa.create_engine(conn_str, echo=False) + # Adding check_same_thread = False after the recording. This can be an issue about + # creating / owner thread when cleaning up sessions, etc. This is a sqlite restriction + # that we probably don't care about in this example. + engine = sa.create_engine(conn_str, echo=False, connect_args={'check_same_thread': False}) __factory = orm.sessionmaker(bind=engine) # noinspection PyUnresolvedReferences diff --git a/app/ch11_migrations/starter/pypi_org/data/downloads.py b/app/ch11_migrations/starter/pypi_org/data/downloads.py index 66068f02..dd8afd65 100644 --- a/app/ch11_migrations/starter/pypi_org/data/downloads.py +++ b/app/ch11_migrations/starter/pypi_org/data/downloads.py @@ -7,8 +7,7 @@ class Download(SqlAlchemyBase): __tablename__ = 'downloads' id: int = sqlalchemy.Column(sqlalchemy.BigInteger, primary_key=True, autoincrement=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) package_id: str = sqlalchemy.Column(sqlalchemy.String, index=True) release_id: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) diff --git a/app/ch11_migrations/starter/pypi_org/data/languages.py b/app/ch11_migrations/starter/pypi_org/data/languages.py index 964b8444..3a73dde0 100644 --- a/app/ch11_migrations/starter/pypi_org/data/languages.py +++ b/app/ch11_migrations/starter/pypi_org/data/languages.py @@ -7,6 +7,5 @@ class ProgrammingLanguage(SqlAlchemyBase): __tablename__ = 'languages' id: str = sqlalchemy.Column(sqlalchemy.String, primary_key=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) description: str = sqlalchemy.Column(sqlalchemy.String) diff --git a/app/ch11_migrations/starter/pypi_org/data/package.py b/app/ch11_migrations/starter/pypi_org/data/package.py index 9e0dcecf..e0e48878 100644 --- a/app/ch11_migrations/starter/pypi_org/data/package.py +++ b/app/ch11_migrations/starter/pypi_org/data/package.py @@ -3,6 +3,8 @@ import sqlalchemy as sa import sqlalchemy.orm as orm +from sqlalchemy.orm import Mapped + from pypi_org.data.modelbase import SqlAlchemyBase from pypi_org.data.releases import Release @@ -25,11 +27,15 @@ class Package(SqlAlchemyBase): license: str = sa.Column(sa.String, index=True) # releases relationship - releases: List[Release] = orm.relation("Release", order_by=[ - Release.major_ver.desc(), - Release.minor_ver.desc(), - Release.build_ver.desc(), - ], back_populates='package') + releases: Mapped[Release] = orm.relationship( + 'Release', + order_by=[ + Release.major_ver.desc(), + Release.minor_ver.desc(), + Release.build_ver.desc(), + ], + back_populates='package', + ) def __repr__(self): return ''.format(self.id) diff --git a/app/ch11_migrations/starter/pypi_org/data/releases.py b/app/ch11_migrations/starter/pypi_org/data/releases.py index 7650e438..4b2db2bf 100644 --- a/app/ch11_migrations/starter/pypi_org/data/releases.py +++ b/app/ch11_migrations/starter/pypi_org/data/releases.py @@ -13,16 +13,14 @@ class Release(SqlAlchemyBase): minor_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) build_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) - created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, - default=datetime.datetime.now, - index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) comment: str = sqlalchemy.Column(sqlalchemy.String) url: str = sqlalchemy.Column(sqlalchemy.String) size: int = sqlalchemy.Column(sqlalchemy.BigInteger) # Package relationship - package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey("packages.id")) - package = orm.relation('Package') + package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey('packages.id')) + package = orm.relationship('Package') @property def version_text(self): diff --git a/app/ch11_migrations/starter/pypi_org/infrastructure/num_convert.py b/app/ch11_migrations/starter/pypi_org/infrastructure/num_convert.py new file mode 100644 index 00000000..4e1d8879 --- /dev/null +++ b/app/ch11_migrations/starter/pypi_org/infrastructure/num_convert.py @@ -0,0 +1,8 @@ +from typing import Optional + + +def try_int(text) -> Optional[int]: + try: + return int(text) + except: + return None diff --git a/app/ch11_migrations/starter/pypi_org/infrastructure/view_modifiers.py b/app/ch11_migrations/starter/pypi_org/infrastructure/view_modifiers.py index 5bb6c178..d944c142 100644 --- a/app/ch11_migrations/starter/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch11_migrations/starter/pypi_org/infrastructure/view_modifiers.py @@ -1,6 +1,8 @@ from functools import wraps import flask +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -10,6 +12,10 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) + + if isinstance(response_val, werkzeug.wrappers.Response): + return response_val + if isinstance(response_val, flask.Response): return response_val @@ -20,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -36,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch11_migrations/starter/pypi_org/services/package_service.py b/app/ch11_migrations/starter/pypi_org/services/package_service.py index e51241ae..6ac445e6 100644 --- a/app/ch11_migrations/starter/pypi_org/services/package_service.py +++ b/app/ch11_migrations/starter/pypi_org/services/package_service.py @@ -9,11 +9,13 @@ def get_latest_releases(limit=10) -> List[Release]: session = db_session.create_session() - releases = session.query(Release). \ - options(sqlalchemy.orm.joinedload(Release.package)). \ - order_by(Release.created_date.desc()). \ - limit(limit). \ - all() + releases = ( + session.query(Release) + .options(sqlalchemy.orm.joinedload(Release.package)) + .order_by(Release.created_date.desc()) + .limit(limit) + .all() + ) session.close() @@ -38,10 +40,12 @@ def get_package_by_id(package_id: str) -> Optional[Package]: session = db_session.create_session() - package = session.query(Package) \ - .options(sqlalchemy.orm.joinedload(Package.releases)) \ - .filter(Package.id == package_id) \ + package = ( + session.query(Package) + .options(sqlalchemy.orm.joinedload(Package.releases)) + .filter(Package.id == package_id) .first() + ) session.close() diff --git a/app/ch11_migrations/starter/pypi_org/views/account_views.py b/app/ch11_migrations/starter/pypi_org/views/account_views.py index 0421324a..55e5df41 100644 --- a/app/ch11_migrations/starter/pypi_org/views/account_views.py +++ b/app/ch11_migrations/starter/pypi_org/views/account_views.py @@ -16,6 +16,7 @@ def index(): # ################### REGISTER ################################# + @blueprint.route('/account/register', methods=['GET']) @response(template_file='account/register.html') def register_get(): @@ -30,6 +31,7 @@ def register_post(): # ################### LOGIN ################################# + @blueprint.route('/account/login', methods=['GET']) @response(template_file='account/login.html') def login_get(): @@ -44,6 +46,7 @@ def login_post(): # ################### LOGOUT ################################# + @blueprint.route('/account/logout') def logout(): return {} diff --git a/app/ch11_migrations/starter/pypi_org/views/cms_views.py b/app/ch11_migrations/starter/pypi_org/views/cms_views.py index f7172d3d..a8b755a8 100644 --- a/app/ch11_migrations/starter/pypi_org/views/cms_views.py +++ b/app/ch11_migrations/starter/pypi_org/views/cms_views.py @@ -9,7 +9,7 @@ @blueprint.route('/') @response(template_file='cms/page.html') def cms_page(full_url: str): - print("Getting CMS page for {}".format(full_url)) + print('Getting CMS page for {}'.format(full_url)) page = cms_service.get_page(full_url) if not page: diff --git a/app/ch11_migrations/starter/pypi_org/views/package_views.py b/app/ch11_migrations/starter/pypi_org/views/package_views.py index 70526f59..4589fe56 100644 --- a/app/ch11_migrations/starter/pypi_org/views/package_views.py +++ b/app/ch11_migrations/starter/pypi_org/views/package_views.py @@ -16,12 +16,12 @@ def package_details(package_name: str): if not package: return flask.abort(status=404) - latest_version = "0.0.0" + latest_version = '0.0.0' latest_release = None is_latest = True if package.releases: - latest_release = package.releases[0] + latest_release = package.releases latest_version = latest_release.version_text return { @@ -36,4 +36,4 @@ def package_details(package_name: str): @blueprint.route('/') def popular(rank: int): print(type(rank), rank) - return "The details for the {}th most popular package".format(rank) + return 'The details for the {}th most popular package'.format(rank) diff --git a/app/ch11_migrations/starter/requirements.piptools b/app/ch11_migrations/starter/requirements.piptools new file mode 100644 index 00000000..2fbbdb71 --- /dev/null +++ b/app/ch11_migrations/starter/requirements.piptools @@ -0,0 +1,4 @@ +flask +progressbar2 +python-dateutil +sqlalchemy diff --git a/app/ch11_migrations/starter/requirements.txt b/app/ch11_migrations/starter/requirements.txt index 9cf703d9..943a0812 100644 --- a/app/ch11_migrations/starter/requirements.txt +++ b/app/ch11_migrations/starter/requirements.txt @@ -1,6 +1,32 @@ -flask -sqlalchemy - -progressbar2 -python-dateutil - +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +markupsafe==3.0.2 + # via + # jinja2 + # werkzeug +progressbar2==4.5.0 + # via -r requirements.piptools +python-dateutil==2.9.0.post0 + # via -r requirements.piptools +python-utils==3.9.1 + # via progressbar2 +six==1.17.0 + # via python-dateutil +sqlalchemy==2.0.38 + # via -r requirements.piptools +typing-extensions==4.12.2 + # via + # python-utils + # sqlalchemy +werkzeug==3.1.3 + # via flask diff --git a/app/ch12-forms/final/.idea/.name b/app/ch12-forms/final/.idea/.name index 868ff2d5..7e120f47 100644 --- a/app/ch12-forms/final/.idea/.name +++ b/app/ch12-forms/final/.idea/.name @@ -1 +1 @@ -flask-html-forms \ No newline at end of file +flask ch12-forms - final \ No newline at end of file diff --git a/app/ch12-forms/final/.idea/dataSources.local.xml b/app/ch12-forms/final/.idea/dataSources.local.xml index 94233bb2..7a996036 100644 --- a/app/ch12-forms/final/.idea/dataSources.local.xml +++ b/app/ch12-forms/final/.idea/dataSources.local.xml @@ -6,8 +6,12 @@ " - false - *:@ + no-auth + + + + + \ No newline at end of file diff --git a/app/ch12-forms/final/.idea/flask-html-forms.iml b/app/ch12-forms/final/.idea/flask ch12-forms - final.iml similarity index 84% rename from app/ch12-forms/final/.idea/flask-html-forms.iml rename to app/ch12-forms/final/.idea/flask ch12-forms - final.iml index 7401e3f0..80a07251 100644 --- a/app/ch12-forms/final/.idea/flask-html-forms.iml +++ b/app/ch12-forms/final/.idea/flask ch12-forms - final.iml @@ -15,7 +15,4 @@ - - \ No newline at end of file diff --git a/app/ch12-forms/final/.idea/inspectionProfiles/Project_Default.xml b/app/ch12-forms/final/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..274983bd --- /dev/null +++ b/app/ch12-forms/final/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,24 @@ + + + + \ No newline at end of file diff --git a/app/ch12-forms/final/.idea/misc.xml b/app/ch12-forms/final/.idea/misc.xml index 09f951f2..4b1ba25d 100644 --- a/app/ch12-forms/final/.idea/misc.xml +++ b/app/ch12-forms/final/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/app/ch12-forms/final/.idea/modules.xml b/app/ch12-forms/final/.idea/modules.xml index bad5f02c..c3d3a250 100644 --- a/app/ch12-forms/final/.idea/modules.xml +++ b/app/ch12-forms/final/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/app/ch12-forms/final/alembic/alembic_helpers.py b/app/ch12-forms/final/alembic/alembic_helpers.py index 561f7a70..6aea52d4 100644 --- a/app/ch12-forms/final/alembic/alembic_helpers.py +++ b/app/ch12-forms/final/alembic/alembic_helpers.py @@ -5,8 +5,7 @@ def table_has_column(table, column): config = op.get_context().config - engine = engine_from_config( - config.get_section(config.config_ini_section), prefix='sqlalchemy.') + engine = engine_from_config(config.get_section(config.config_ini_section), prefix='sqlalchemy.') insp = reflection.Inspector.from_engine(engine) has_column = False for col in insp.get_columns(table): diff --git a/app/ch12-forms/final/alembic/env.py b/app/ch12-forms/final/alembic/env.py index 27ec49fb..1fdc9cab 100644 --- a/app/ch12-forms/final/alembic/env.py +++ b/app/ch12-forms/final/alembic/env.py @@ -26,6 +26,7 @@ sys.path.insert(0, folder) from pypi_org.data.modelbase import SqlAlchemyBase + # noinspection PyUnresolvedReferences import pypi_org.data.__all_models @@ -49,10 +50,8 @@ def run_migrations_offline(): script output. """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, target_metadata=target_metadata, literal_binds=True - ) + url = config.get_main_option('sqlalchemy.url') + context.configure(url=url, target_metadata=target_metadata, literal_binds=True) with context.begin_transaction(): context.run_migrations() @@ -67,14 +66,12 @@ def run_migrations_online(): """ connectable = engine_from_config( config.get_section(config.config_ini_section), - prefix="sqlalchemy.", + prefix='sqlalchemy.', poolclass=pool.NullPool, ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() diff --git a/app/ch12-forms/final/alembic/versions/722c82f0097c_added_auditing_table.py b/app/ch12-forms/final/alembic/versions/722c82f0097c_added_auditing_table.py index 2c3e2a39..7c6e892f 100644 --- a/app/ch12-forms/final/alembic/versions/722c82f0097c_added_auditing_table.py +++ b/app/ch12-forms/final/alembic/versions/722c82f0097c_added_auditing_table.py @@ -18,11 +18,12 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('auditing', - sa.Column('id', sa.String(), nullable=False), - sa.Column('created_date', sa.DateTime(), nullable=True), - sa.Column('description', sa.String(), nullable=True), - sa.PrimaryKeyConstraint('id') + op.create_table( + 'auditing', + sa.Column('id', sa.String(), nullable=False), + sa.Column('created_date', sa.DateTime(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id'), ) op.create_index(op.f('ix_auditing_created_date'), 'auditing', ['created_date'], unique=False) # ### end Alembic commands ### diff --git a/app/ch12-forms/final/pypi_org/app.py b/app/ch12-forms/final/pypi_org/app.py index dc44d0cf..0fbd8016 100644 --- a/app/ch12-forms/final/pypi_org/app.py +++ b/app/ch12-forms/final/pypi_org/app.py @@ -1,6 +1,7 @@ import os import sys import flask + folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, folder) @@ -10,16 +11,17 @@ def main(): + configure() + app.run(debug=True) + + +def configure(): register_blueprints() setup_db() - app.run(debug=True) def setup_db(): - db_file = os.path.join( - os.path.dirname(__file__), - 'db', - 'pypi.sqlite') + db_file = os.path.join(os.path.dirname(__file__), 'db', 'pypi.sqlite') db_session.global_init(db_file) @@ -38,3 +40,5 @@ def register_blueprints(): if __name__ == '__main__': main() +else: + configure() diff --git a/app/ch12-forms/final/pypi_org/bin/basic_inserts.py b/app/ch12-forms/final/pypi_org/bin/basic_inserts.py index 3a2f360d..6d04f551 100644 --- a/app/ch12-forms/final/pypi_org/bin/basic_inserts.py +++ b/app/ch12-forms/final/pypi_org/bin/basic_inserts.py @@ -1,14 +1,13 @@ import os import sys +# Make it run more easily outside of PyCharm +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + import pypi_org.data.db_session as db_session from pypi_org.data.package import Package from pypi_org.data.releases import Release -# Make it run more easily outside of PyCharm -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) - def main(): init_db() @@ -20,24 +19,24 @@ def insert_a_package(): p = Package() p.id = input('Package id / name: ').strip().lower() - p.summary = input("Package summary: ").strip() - p.author_name = input("Author: ").strip() - p.license = input("License: ").strip() + p.summary = input('Package summary: ').strip() + p.author_name = input('Author: ').strip() + p.license = input('License: ').strip() - print("Release 1:") + print('Release 1:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) - print("Release 2:") + print('Release 2:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) session = db_session.create_session() diff --git a/app/ch12-forms/final/pypi_org/bin/load_data.py b/app/ch12-forms/final/pypi_org/bin/load_data.py index 15237f18..3af7a03d 100644 --- a/app/ch12-forms/final/pypi_org/bin/load_data.py +++ b/app/ch12-forms/final/pypi_org/bin/load_data.py @@ -7,9 +7,9 @@ import progressbar from dateutil.parser import parse -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) +from pypi_org.infrastructure.num_convert import try_int import pypi_org.data.db_session as db_session from pypi_org.data.languages import ProgrammingLanguage from pypi_org.data.licenses import License @@ -39,7 +39,7 @@ def main(): def do_import_languages(file_data: List[dict]): imported = set() - print("Importing languages ... ", flush=True) + print('Importing languages ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -74,7 +74,7 @@ def do_import_languages(file_data: List[dict]): def do_import_licenses(file_data: List[dict]): imported = set() - print("Importing licenses ... ", flush=True) + print('Importing licenses ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -100,17 +100,17 @@ def do_import_licenses(file_data: List[dict]): def do_summary(): session = db_session.create_session() - print("Final numbers:") - print("Users: {:,}".format(session.query(User).count())) - print("Packages: {:,}".format(session.query(Package).count())) - print("Releases: {:,}".format(session.query(Release).count())) - print("Maintainers: {:,}".format(session.query(Maintainer).count())) - print("Languages: {:,}".format(session.query(ProgrammingLanguage).count())) - print("Licenses: {:,}".format(session.query(License).count())) + print('Final numbers:') + print('Users: {:,}'.format(session.query(User).count())) + print('Packages: {:,}'.format(session.query(Package).count())) + print('Releases: {:,}'.format(session.query(Release).count())) + print('Maintainers: {:,}'.format(session.query(Maintainer).count())) + print('Languages: {:,}'.format(session.query(ProgrammingLanguage).count())) + print('Licenses: {:,}'.format(session.query(License).count())) def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: - print("Importing users ... ", flush=True) + print('Importing users ... ', flush=True) with progressbar.ProgressBar(max_value=len(user_lookup)) as bar: for idx, (email, name) in enumerate(user_lookup.items()): session = db_session.create_session() @@ -134,29 +134,29 @@ def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: def do_import_packages(file_data: List[dict], user_lookup: Dict[str, User]): errored_packages = [] - print("Importing packages and releases ... ", flush=True) + print('Importing packages and releases ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): try: load_package(p, user_lookup) bar.update(idx) except Exception as x: - errored_packages.append((p, " *** Errored out for package {}, {}".format(p.get('package_name'), x))) + errored_packages.append((p, ' *** Errored out for package {}, {}'.format(p.get('package_name'), x))) raise sys.stderr.flush() sys.stdout.flush() print() - print("Completed packages with {} errors.".format(len(errored_packages))) - for (p, txt) in errored_packages: + print('Completed packages with {} errors.'.format(len(errored_packages))) + for p, txt in errored_packages: print(txt) def do_load_files() -> List[dict]: data_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../../data/pypi-top-100')) - print("Loading files from {}".format(data_path)) + print('Loading files from {}'.format(data_path)) files = get_file_names(data_path) - print("Found {:,} files, loading ...".format(len(files)), flush=True) - time.sleep(.1) + print('Found {:,} files, loading ...'.format(len(files)), flush=True) + time.sleep(0.1) file_data = [] with progressbar.ProgressBar(max_value=len(files)) as bar: @@ -171,7 +171,7 @@ def do_load_files() -> List[dict]: def find_users(data: List[dict]) -> dict: - print("Discovering users...", flush=True) + print('Discovering users...', flush=True) found_users = {} with progressbar.ProgressBar(max_value=len(data)) as bar: @@ -184,7 +184,7 @@ def find_users(data: List[dict]) -> dict: sys.stderr.flush() sys.stdout.flush() print() - print("Discovered {:,} users".format(len(found_users))) + print('Discovered {:,} users'.format(len(found_users))) print() return found_users @@ -215,7 +215,7 @@ def load_file_data(filename: str) -> dict: with open(filename, 'r', encoding='utf-8') as fin: data = json.load(fin) except Exception as x: - print("ERROR in file: {}, details: {}".format(filename, x), flush=True) + print('ERROR in file: {}, details: {}'.format(filename, x), flush=True) raise return data @@ -233,7 +233,7 @@ def load_package(data: dict, user_lookup: Dict[str, User]): p.author = info.get('author') p.author_email = info.get('author_email') - releases = build_releases(p.id, data.get("releases", {})) + releases = build_releases(p.id, data.get('releases', {})) if releases: p.created_date = releases[0].created_date @@ -283,18 +283,13 @@ def detect_license(license_text: str) -> Optional[str]: license_text = license_text.strip() if len(license_text) > 100 or '\n' in license_text: - return "CUSTOM" + return 'CUSTOM' - license_text = license_text \ - .replace('Software License', '') \ - .replace('License', '') + license_text = license_text.replace('Software License', '').replace('License', '') if '::' in license_text: # E.g. 'License :: OSI Approved :: Apache Software License' - return license_text \ - .split(':')[-1] \ - .replace(' ', ' ') \ - .strip() + return license_text.split(':')[-1].replace(' ', ' ').strip() return license_text.strip() @@ -339,13 +334,6 @@ def make_version_num(version_text): return major, minor, build -def try_int(text) -> int: - try: - return int(text) - except: - return 0 - - def init_db(): top_folder = os.path.dirname(__file__) rel_file = os.path.join('..', 'db', 'pypi.sqlite') @@ -357,9 +345,7 @@ def get_file_names(data_path: str) -> List[str]: files = [] for f in os.listdir(data_path): if f.endswith('.json'): - files.append( - os.path.abspath(os.path.join(data_path, f)) - ) + files.append(os.path.abspath(os.path.join(data_path, f))) files.sort() return files diff --git a/app/ch12-forms/final/pypi_org/data/__all_models.py b/app/ch12-forms/final/pypi_org/data/__all_models.py index f399ef54..ca4804ac 100644 --- a/app/ch12-forms/final/pypi_org/data/__all_models.py +++ b/app/ch12-forms/final/pypi_org/data/__all_models.py @@ -5,17 +5,24 @@ # noinspection PyUnresolvedReferences import pypi_org.data.audit + # noinspection PyUnresolvedReferences import pypi_org.data.downloads + # noinspection PyUnresolvedReferences import pypi_org.data.languages + # noinspection PyUnresolvedReferences import pypi_org.data.licenses + # noinspection PyUnresolvedReferences import pypi_org.data.maintainers + # noinspection PyUnresolvedReferences import pypi_org.data.package + # noinspection PyUnresolvedReferences import pypi_org.data.releases + # noinspection PyUnresolvedReferences import pypi_org.data.users diff --git a/app/ch12-forms/final/pypi_org/data/db_session.py b/app/ch12-forms/final/pypi_org/data/db_session.py index 50861b3b..62391b85 100644 --- a/app/ch12-forms/final/pypi_org/data/db_session.py +++ b/app/ch12-forms/final/pypi_org/data/db_session.py @@ -14,12 +14,15 @@ def global_init(db_file: str): return if not db_file or not db_file.strip(): - raise Exception("You must specify a db file.") + raise Exception('You must specify a db file.') conn_str = 'sqlite:///' + db_file.strip() - print("Connecting to DB with {}".format(conn_str)) + print('Connecting to DB with {}'.format(conn_str)) - engine = sa.create_engine(conn_str, echo=False) + # Adding check_same_thread = False after the recording. This can be an issue about + # creating / owner thread when cleaning up sessions, etc. This is a sqlite restriction + # that we probably don't care about in this example. + engine = sa.create_engine(conn_str, echo=False, connect_args={'check_same_thread': False}) __factory = orm.sessionmaker(bind=engine) # noinspection PyUnresolvedReferences diff --git a/app/ch12-forms/final/pypi_org/data/downloads.py b/app/ch12-forms/final/pypi_org/data/downloads.py index 66068f02..dd8afd65 100644 --- a/app/ch12-forms/final/pypi_org/data/downloads.py +++ b/app/ch12-forms/final/pypi_org/data/downloads.py @@ -7,8 +7,7 @@ class Download(SqlAlchemyBase): __tablename__ = 'downloads' id: int = sqlalchemy.Column(sqlalchemy.BigInteger, primary_key=True, autoincrement=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) package_id: str = sqlalchemy.Column(sqlalchemy.String, index=True) release_id: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) diff --git a/app/ch12-forms/final/pypi_org/data/languages.py b/app/ch12-forms/final/pypi_org/data/languages.py index 964b8444..3a73dde0 100644 --- a/app/ch12-forms/final/pypi_org/data/languages.py +++ b/app/ch12-forms/final/pypi_org/data/languages.py @@ -7,6 +7,5 @@ class ProgrammingLanguage(SqlAlchemyBase): __tablename__ = 'languages' id: str = sqlalchemy.Column(sqlalchemy.String, primary_key=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) description: str = sqlalchemy.Column(sqlalchemy.String) diff --git a/app/ch12-forms/final/pypi_org/data/package.py b/app/ch12-forms/final/pypi_org/data/package.py index c3420d1b..e0e48878 100644 --- a/app/ch12-forms/final/pypi_org/data/package.py +++ b/app/ch12-forms/final/pypi_org/data/package.py @@ -3,6 +3,8 @@ import sqlalchemy as sa import sqlalchemy.orm as orm +from sqlalchemy.orm import Mapped + from pypi_org.data.modelbase import SqlAlchemyBase from pypi_org.data.releases import Release @@ -12,7 +14,6 @@ class Package(SqlAlchemyBase): id: str = sa.Column(sa.String, primary_key=True) created_date: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) - last_updated: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) summary: str = sa.Column(sa.String, nullable=False) description: str = sa.Column(sa.String, nullable=True) @@ -26,11 +27,15 @@ class Package(SqlAlchemyBase): license: str = sa.Column(sa.String, index=True) # releases relationship - releases: List[Release] = orm.relation("Release", order_by=[ - Release.major_ver.desc(), - Release.minor_ver.desc(), - Release.build_ver.desc(), - ], back_populates='package') + releases: Mapped[Release] = orm.relationship( + 'Release', + order_by=[ + Release.major_ver.desc(), + Release.minor_ver.desc(), + Release.build_ver.desc(), + ], + back_populates='package', + ) def __repr__(self): return ''.format(self.id) diff --git a/app/ch12-forms/final/pypi_org/data/releases.py b/app/ch12-forms/final/pypi_org/data/releases.py index 7650e438..4b2db2bf 100644 --- a/app/ch12-forms/final/pypi_org/data/releases.py +++ b/app/ch12-forms/final/pypi_org/data/releases.py @@ -13,16 +13,14 @@ class Release(SqlAlchemyBase): minor_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) build_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) - created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, - default=datetime.datetime.now, - index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) comment: str = sqlalchemy.Column(sqlalchemy.String) url: str = sqlalchemy.Column(sqlalchemy.String) size: int = sqlalchemy.Column(sqlalchemy.BigInteger) # Package relationship - package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey("packages.id")) - package = orm.relation('Package') + package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey('packages.id')) + package = orm.relationship('Package') @property def version_text(self): diff --git a/app/ch12-forms/final/pypi_org/infrastructure/cookie_auth.py b/app/ch12-forms/final/pypi_org/infrastructure/cookie_auth.py index 64dc85a7..d76a4d30 100644 --- a/app/ch12-forms/final/pypi_org/infrastructure/cookie_auth.py +++ b/app/ch12-forms/final/pypi_org/infrastructure/cookie_auth.py @@ -1,5 +1,4 @@ import hashlib -from datetime import timedelta from typing import Optional from flask import Request @@ -12,8 +11,8 @@ def set_auth(response: Response, user_id: int): hash_val = __hash_text(str(user_id)) - val = "{}:{}".format(user_id, hash_val) - response.set_cookie(auth_cookie_name, val) + val = '{}:{}'.format(user_id, hash_val) + response.set_cookie(auth_cookie_name, val, secure=False, httponly=True, samesite='Lax') def __hash_text(text: str) -> str: @@ -21,10 +20,6 @@ def __hash_text(text: str) -> str: return hashlib.sha512(text.encode('utf-8')).hexdigest() -def __add_cookie_callback(_, response: Response, name: str, value: str): - response.set_cookie(name, value, max_age=timedelta(days=30)) - - def get_user_id_via_auth_cookie(request: Request) -> Optional[int]: if auth_cookie_name not in request.cookies: return None @@ -38,7 +33,7 @@ def get_user_id_via_auth_cookie(request: Request) -> Optional[int]: hash_val = parts[1] hash_val_check = __hash_text(user_id) if hash_val != hash_val_check: - print("Warning: Hash mismatch, invalid cookie value") + print('Warning: Hash mismatch, invalid cookie value') return None return try_int(user_id) diff --git a/app/ch12-forms/final/pypi_org/infrastructure/num_convert.py b/app/ch12-forms/final/pypi_org/infrastructure/num_convert.py new file mode 100644 index 00000000..705fbd60 --- /dev/null +++ b/app/ch12-forms/final/pypi_org/infrastructure/num_convert.py @@ -0,0 +1,8 @@ +from typing import Optional + + +def try_int(text) -> Optional[int]: + try: + return int(text) + except: + return 0 diff --git a/app/ch12-forms/final/pypi_org/infrastructure/request_dict.py b/app/ch12-forms/final/pypi_org/infrastructure/request_dict.py index 495fd979..d44ae574 100644 --- a/app/ch12-forms/final/pypi_org/infrastructure/request_dict.py +++ b/app/ch12-forms/final/pypi_org/infrastructure/request_dict.py @@ -1,4 +1,5 @@ import flask +from werkzeug.datastructures import MultiDict class RequestDictionary(dict): @@ -13,11 +14,22 @@ def __getattr__(self, key): def create(default_val=None, **route_args) -> RequestDictionary: request = flask.request + # Adding this retro actively. Some folks are experiencing issues where they + # are getting a list rather than plain dict. I think it's from multiple + # entries in the multidict. This should fix it. + args = request.args + if isinstance(request.args, MultiDict): + args = request.args.to_dict() + + form = request.form + if isinstance(request.args, MultiDict): + form = request.form.to_dict() + data = { - **request.args, # The key/value pairs in the URL query string + **args, # The key/value pairs in the URL query string **request.headers, # Header values - **request.form, # The key/value pairs in the body, from a HTML post form - **route_args # And additional arguments the method access, if they want them merged. + **form, # The key/value pairs in the body, from a HTML post form + **route_args, # And additional arguments the method access, if they want them merged. } return RequestDictionary(data, default_val=default_val) diff --git a/app/ch12-forms/final/pypi_org/infrastructure/view_modifiers.py b/app/ch12-forms/final/pypi_org/infrastructure/view_modifiers.py index 82ce5baf..d944c142 100644 --- a/app/ch12-forms/final/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch12-forms/final/pypi_org/infrastructure/view_modifiers.py @@ -1,7 +1,8 @@ from functools import wraps import flask -import werkzeug.wrappers.response +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -11,9 +12,11 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) - if isinstance(response_val, flask.Response): + + if isinstance(response_val, werkzeug.wrappers.Response): return response_val - if isinstance(response_val, werkzeug.wrappers.response.Response): + + if isinstance(response_val, flask.Response): return response_val if isinstance(response_val, dict): @@ -23,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -39,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch12-forms/final/pypi_org/services/package_service.py b/app/ch12-forms/final/pypi_org/services/package_service.py index e51241ae..6ac445e6 100644 --- a/app/ch12-forms/final/pypi_org/services/package_service.py +++ b/app/ch12-forms/final/pypi_org/services/package_service.py @@ -9,11 +9,13 @@ def get_latest_releases(limit=10) -> List[Release]: session = db_session.create_session() - releases = session.query(Release). \ - options(sqlalchemy.orm.joinedload(Release.package)). \ - order_by(Release.created_date.desc()). \ - limit(limit). \ - all() + releases = ( + session.query(Release) + .options(sqlalchemy.orm.joinedload(Release.package)) + .order_by(Release.created_date.desc()) + .limit(limit) + .all() + ) session.close() @@ -38,10 +40,12 @@ def get_package_by_id(package_id: str) -> Optional[Package]: session = db_session.create_session() - package = session.query(Package) \ - .options(sqlalchemy.orm.joinedload(Package.releases)) \ - .filter(Package.id == package_id) \ + package = ( + session.query(Package) + .options(sqlalchemy.orm.joinedload(Package.releases)) + .filter(Package.id == package_id) .first() + ) session.close() diff --git a/app/ch12-forms/final/pypi_org/views/account_views.py b/app/ch12-forms/final/pypi_org/views/account_views.py index f127310b..877231ba 100644 --- a/app/ch12-forms/final/pypi_org/views/account_views.py +++ b/app/ch12-forms/final/pypi_org/views/account_views.py @@ -30,6 +30,7 @@ def index(): # ################### REGISTER ################################# + @blueprint.route('/account/register', methods=['GET']) @response(template_file='account/register.html') def register_get(): @@ -52,7 +53,7 @@ def register_post(): 'name': name, 'email': email, 'password': password, - 'error': "Some required fields are missing.", + 'error': 'Some required fields are missing.', 'user_id': cookie_auth.get_user_id_via_auth_cookie(flask.request), } @@ -62,7 +63,7 @@ def register_post(): 'name': name, 'email': email, 'password': password, - 'error': "A user with that email already exists.", + 'error': 'A user with that email already exists.', 'user_id': cookie_auth.get_user_id_via_auth_cookie(flask.request), } @@ -74,6 +75,7 @@ def register_post(): # ################### LOGIN ################################# + @blueprint.route('/account/login', methods=['GET']) @response(template_file='account/login.html') def login_get(): @@ -92,7 +94,7 @@ def login_post(): return { 'email': email, 'password': password, - 'error': "Some required fields are missing.", + 'error': 'Some required fields are missing.', 'user_id': cookie_auth.get_user_id_via_auth_cookie(flask.request), } @@ -101,7 +103,7 @@ def login_post(): return { 'email': email, 'password': password, - 'error': "The account does not exist or the password is wrong.", + 'error': 'The account does not exist or the password is wrong.', 'user_id': cookie_auth.get_user_id_via_auth_cookie(flask.request), } @@ -113,6 +115,7 @@ def login_post(): # ################### LOGOUT ################################# + @blueprint.route('/account/logout') def logout(): resp = flask.redirect('/') diff --git a/app/ch12-forms/final/pypi_org/views/cms_views.py b/app/ch12-forms/final/pypi_org/views/cms_views.py index f7172d3d..a8b755a8 100644 --- a/app/ch12-forms/final/pypi_org/views/cms_views.py +++ b/app/ch12-forms/final/pypi_org/views/cms_views.py @@ -9,7 +9,7 @@ @blueprint.route('/') @response(template_file='cms/page.html') def cms_page(full_url: str): - print("Getting CMS page for {}".format(full_url)) + print('Getting CMS page for {}'.format(full_url)) page = cms_service.get_page(full_url) if not page: diff --git a/app/ch12-forms/final/pypi_org/views/package_views.py b/app/ch12-forms/final/pypi_org/views/package_views.py index f1ad7811..e634d845 100644 --- a/app/ch12-forms/final/pypi_org/views/package_views.py +++ b/app/ch12-forms/final/pypi_org/views/package_views.py @@ -17,12 +17,12 @@ def package_details(package_name: str): if not package: return flask.abort(status=404) - latest_version = "0.0.0" + latest_version = '0.0.0' latest_release = None is_latest = True if package.releases: - latest_release = package.releases[0] + latest_release = package.releases latest_version = latest_release.version_text return { @@ -38,4 +38,4 @@ def package_details(package_name: str): @blueprint.route('/') def popular(rank: int): print(type(rank), rank) - return "The details for the {}th most popular package".format(rank) + return 'The details for the {}th most popular package'.format(rank) diff --git a/app/ch12-forms/final/requirements.piptools b/app/ch12-forms/final/requirements.piptools new file mode 100644 index 00000000..d13bf0c8 --- /dev/null +++ b/app/ch12-forms/final/requirements.piptools @@ -0,0 +1,6 @@ +alembic +flask +passlib +progressbar2 +python-dateutil +sqlalchemy diff --git a/app/ch12-forms/final/requirements.txt b/app/ch12-forms/final/requirements.txt index e10973d8..19fc8c45 100644 --- a/app/ch12-forms/final/requirements.txt +++ b/app/ch12-forms/final/requirements.txt @@ -1,8 +1,42 @@ -werkzeug -flask -sqlalchemy - -progressbar2 -python-dateutil -passlib - +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +alembic==1.14.1 + # via -r requirements.piptools +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +mako==1.3.9 + # via alembic +markupsafe==3.0.2 + # via + # jinja2 + # mako + # werkzeug +passlib==1.7.4 + # via -r requirements.piptools +progressbar2==4.5.0 + # via -r requirements.piptools +python-dateutil==2.9.0.post0 + # via -r requirements.piptools +python-utils==3.9.1 + # via progressbar2 +six==1.17.0 + # via python-dateutil +sqlalchemy==2.0.38 + # via + # -r requirements.piptools + # alembic +typing-extensions==4.12.2 + # via + # alembic + # python-utils + # sqlalchemy +werkzeug==3.1.3 + # via flask diff --git a/app/ch12-forms/starter/.idea/flask ch12-forms - starter.iml b/app/ch12-forms/starter/.idea/flask ch12-forms - starter.iml new file mode 100644 index 00000000..2fbaf4e6 --- /dev/null +++ b/app/ch12-forms/starter/.idea/flask ch12-forms - starter.iml @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/ch12-forms/starter/alembic/alembic_helpers.py b/app/ch12-forms/starter/alembic/alembic_helpers.py index 561f7a70..6aea52d4 100644 --- a/app/ch12-forms/starter/alembic/alembic_helpers.py +++ b/app/ch12-forms/starter/alembic/alembic_helpers.py @@ -5,8 +5,7 @@ def table_has_column(table, column): config = op.get_context().config - engine = engine_from_config( - config.get_section(config.config_ini_section), prefix='sqlalchemy.') + engine = engine_from_config(config.get_section(config.config_ini_section), prefix='sqlalchemy.') insp = reflection.Inspector.from_engine(engine) has_column = False for col in insp.get_columns(table): diff --git a/app/ch12-forms/starter/alembic/env.py b/app/ch12-forms/starter/alembic/env.py index 27ec49fb..1fdc9cab 100644 --- a/app/ch12-forms/starter/alembic/env.py +++ b/app/ch12-forms/starter/alembic/env.py @@ -26,6 +26,7 @@ sys.path.insert(0, folder) from pypi_org.data.modelbase import SqlAlchemyBase + # noinspection PyUnresolvedReferences import pypi_org.data.__all_models @@ -49,10 +50,8 @@ def run_migrations_offline(): script output. """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, target_metadata=target_metadata, literal_binds=True - ) + url = config.get_main_option('sqlalchemy.url') + context.configure(url=url, target_metadata=target_metadata, literal_binds=True) with context.begin_transaction(): context.run_migrations() @@ -67,14 +66,12 @@ def run_migrations_online(): """ connectable = engine_from_config( config.get_section(config.config_ini_section), - prefix="sqlalchemy.", + prefix='sqlalchemy.', poolclass=pool.NullPool, ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() diff --git a/app/ch12-forms/starter/alembic/versions/722c82f0097c_added_auditing_table.py b/app/ch12-forms/starter/alembic/versions/722c82f0097c_added_auditing_table.py index 2c3e2a39..7c6e892f 100644 --- a/app/ch12-forms/starter/alembic/versions/722c82f0097c_added_auditing_table.py +++ b/app/ch12-forms/starter/alembic/versions/722c82f0097c_added_auditing_table.py @@ -18,11 +18,12 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('auditing', - sa.Column('id', sa.String(), nullable=False), - sa.Column('created_date', sa.DateTime(), nullable=True), - sa.Column('description', sa.String(), nullable=True), - sa.PrimaryKeyConstraint('id') + op.create_table( + 'auditing', + sa.Column('id', sa.String(), nullable=False), + sa.Column('created_date', sa.DateTime(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id'), ) op.create_index(op.f('ix_auditing_created_date'), 'auditing', ['created_date'], unique=False) # ### end Alembic commands ### diff --git a/app/ch12-forms/starter/pypi_org/app.py b/app/ch12-forms/starter/pypi_org/app.py index 4b39b374..0315d74a 100644 --- a/app/ch12-forms/starter/pypi_org/app.py +++ b/app/ch12-forms/starter/pypi_org/app.py @@ -1,6 +1,7 @@ import os import sys import flask + folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, folder) @@ -16,10 +17,7 @@ def main(): def setup_db(): - db_file = os.path.join( - os.path.dirname(__file__), - 'db', - 'pypi.sqlite') + db_file = os.path.join(os.path.dirname(__file__), 'db', 'pypi.sqlite') db_session.global_init(db_file) diff --git a/app/ch12-forms/starter/pypi_org/bin/basic_inserts.py b/app/ch12-forms/starter/pypi_org/bin/basic_inserts.py index 3a2f360d..6d04f551 100644 --- a/app/ch12-forms/starter/pypi_org/bin/basic_inserts.py +++ b/app/ch12-forms/starter/pypi_org/bin/basic_inserts.py @@ -1,14 +1,13 @@ import os import sys +# Make it run more easily outside of PyCharm +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + import pypi_org.data.db_session as db_session from pypi_org.data.package import Package from pypi_org.data.releases import Release -# Make it run more easily outside of PyCharm -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) - def main(): init_db() @@ -20,24 +19,24 @@ def insert_a_package(): p = Package() p.id = input('Package id / name: ').strip().lower() - p.summary = input("Package summary: ").strip() - p.author_name = input("Author: ").strip() - p.license = input("License: ").strip() + p.summary = input('Package summary: ').strip() + p.author_name = input('Author: ').strip() + p.license = input('License: ').strip() - print("Release 1:") + print('Release 1:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) - print("Release 2:") + print('Release 2:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) session = db_session.create_session() diff --git a/app/ch12-forms/starter/pypi_org/bin/load_data.py b/app/ch12-forms/starter/pypi_org/bin/load_data.py index 15237f18..3af7a03d 100644 --- a/app/ch12-forms/starter/pypi_org/bin/load_data.py +++ b/app/ch12-forms/starter/pypi_org/bin/load_data.py @@ -7,9 +7,9 @@ import progressbar from dateutil.parser import parse -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) +from pypi_org.infrastructure.num_convert import try_int import pypi_org.data.db_session as db_session from pypi_org.data.languages import ProgrammingLanguage from pypi_org.data.licenses import License @@ -39,7 +39,7 @@ def main(): def do_import_languages(file_data: List[dict]): imported = set() - print("Importing languages ... ", flush=True) + print('Importing languages ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -74,7 +74,7 @@ def do_import_languages(file_data: List[dict]): def do_import_licenses(file_data: List[dict]): imported = set() - print("Importing licenses ... ", flush=True) + print('Importing licenses ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -100,17 +100,17 @@ def do_import_licenses(file_data: List[dict]): def do_summary(): session = db_session.create_session() - print("Final numbers:") - print("Users: {:,}".format(session.query(User).count())) - print("Packages: {:,}".format(session.query(Package).count())) - print("Releases: {:,}".format(session.query(Release).count())) - print("Maintainers: {:,}".format(session.query(Maintainer).count())) - print("Languages: {:,}".format(session.query(ProgrammingLanguage).count())) - print("Licenses: {:,}".format(session.query(License).count())) + print('Final numbers:') + print('Users: {:,}'.format(session.query(User).count())) + print('Packages: {:,}'.format(session.query(Package).count())) + print('Releases: {:,}'.format(session.query(Release).count())) + print('Maintainers: {:,}'.format(session.query(Maintainer).count())) + print('Languages: {:,}'.format(session.query(ProgrammingLanguage).count())) + print('Licenses: {:,}'.format(session.query(License).count())) def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: - print("Importing users ... ", flush=True) + print('Importing users ... ', flush=True) with progressbar.ProgressBar(max_value=len(user_lookup)) as bar: for idx, (email, name) in enumerate(user_lookup.items()): session = db_session.create_session() @@ -134,29 +134,29 @@ def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: def do_import_packages(file_data: List[dict], user_lookup: Dict[str, User]): errored_packages = [] - print("Importing packages and releases ... ", flush=True) + print('Importing packages and releases ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): try: load_package(p, user_lookup) bar.update(idx) except Exception as x: - errored_packages.append((p, " *** Errored out for package {}, {}".format(p.get('package_name'), x))) + errored_packages.append((p, ' *** Errored out for package {}, {}'.format(p.get('package_name'), x))) raise sys.stderr.flush() sys.stdout.flush() print() - print("Completed packages with {} errors.".format(len(errored_packages))) - for (p, txt) in errored_packages: + print('Completed packages with {} errors.'.format(len(errored_packages))) + for p, txt in errored_packages: print(txt) def do_load_files() -> List[dict]: data_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../../data/pypi-top-100')) - print("Loading files from {}".format(data_path)) + print('Loading files from {}'.format(data_path)) files = get_file_names(data_path) - print("Found {:,} files, loading ...".format(len(files)), flush=True) - time.sleep(.1) + print('Found {:,} files, loading ...'.format(len(files)), flush=True) + time.sleep(0.1) file_data = [] with progressbar.ProgressBar(max_value=len(files)) as bar: @@ -171,7 +171,7 @@ def do_load_files() -> List[dict]: def find_users(data: List[dict]) -> dict: - print("Discovering users...", flush=True) + print('Discovering users...', flush=True) found_users = {} with progressbar.ProgressBar(max_value=len(data)) as bar: @@ -184,7 +184,7 @@ def find_users(data: List[dict]) -> dict: sys.stderr.flush() sys.stdout.flush() print() - print("Discovered {:,} users".format(len(found_users))) + print('Discovered {:,} users'.format(len(found_users))) print() return found_users @@ -215,7 +215,7 @@ def load_file_data(filename: str) -> dict: with open(filename, 'r', encoding='utf-8') as fin: data = json.load(fin) except Exception as x: - print("ERROR in file: {}, details: {}".format(filename, x), flush=True) + print('ERROR in file: {}, details: {}'.format(filename, x), flush=True) raise return data @@ -233,7 +233,7 @@ def load_package(data: dict, user_lookup: Dict[str, User]): p.author = info.get('author') p.author_email = info.get('author_email') - releases = build_releases(p.id, data.get("releases", {})) + releases = build_releases(p.id, data.get('releases', {})) if releases: p.created_date = releases[0].created_date @@ -283,18 +283,13 @@ def detect_license(license_text: str) -> Optional[str]: license_text = license_text.strip() if len(license_text) > 100 or '\n' in license_text: - return "CUSTOM" + return 'CUSTOM' - license_text = license_text \ - .replace('Software License', '') \ - .replace('License', '') + license_text = license_text.replace('Software License', '').replace('License', '') if '::' in license_text: # E.g. 'License :: OSI Approved :: Apache Software License' - return license_text \ - .split(':')[-1] \ - .replace(' ', ' ') \ - .strip() + return license_text.split(':')[-1].replace(' ', ' ').strip() return license_text.strip() @@ -339,13 +334,6 @@ def make_version_num(version_text): return major, minor, build -def try_int(text) -> int: - try: - return int(text) - except: - return 0 - - def init_db(): top_folder = os.path.dirname(__file__) rel_file = os.path.join('..', 'db', 'pypi.sqlite') @@ -357,9 +345,7 @@ def get_file_names(data_path: str) -> List[str]: files = [] for f in os.listdir(data_path): if f.endswith('.json'): - files.append( - os.path.abspath(os.path.join(data_path, f)) - ) + files.append(os.path.abspath(os.path.join(data_path, f))) files.sort() return files diff --git a/app/ch12-forms/starter/pypi_org/data/__all_models.py b/app/ch12-forms/starter/pypi_org/data/__all_models.py index f399ef54..ca4804ac 100644 --- a/app/ch12-forms/starter/pypi_org/data/__all_models.py +++ b/app/ch12-forms/starter/pypi_org/data/__all_models.py @@ -5,17 +5,24 @@ # noinspection PyUnresolvedReferences import pypi_org.data.audit + # noinspection PyUnresolvedReferences import pypi_org.data.downloads + # noinspection PyUnresolvedReferences import pypi_org.data.languages + # noinspection PyUnresolvedReferences import pypi_org.data.licenses + # noinspection PyUnresolvedReferences import pypi_org.data.maintainers + # noinspection PyUnresolvedReferences import pypi_org.data.package + # noinspection PyUnresolvedReferences import pypi_org.data.releases + # noinspection PyUnresolvedReferences import pypi_org.data.users diff --git a/app/ch12-forms/starter/pypi_org/data/db_session.py b/app/ch12-forms/starter/pypi_org/data/db_session.py index 50861b3b..62391b85 100644 --- a/app/ch12-forms/starter/pypi_org/data/db_session.py +++ b/app/ch12-forms/starter/pypi_org/data/db_session.py @@ -14,12 +14,15 @@ def global_init(db_file: str): return if not db_file or not db_file.strip(): - raise Exception("You must specify a db file.") + raise Exception('You must specify a db file.') conn_str = 'sqlite:///' + db_file.strip() - print("Connecting to DB with {}".format(conn_str)) + print('Connecting to DB with {}'.format(conn_str)) - engine = sa.create_engine(conn_str, echo=False) + # Adding check_same_thread = False after the recording. This can be an issue about + # creating / owner thread when cleaning up sessions, etc. This is a sqlite restriction + # that we probably don't care about in this example. + engine = sa.create_engine(conn_str, echo=False, connect_args={'check_same_thread': False}) __factory = orm.sessionmaker(bind=engine) # noinspection PyUnresolvedReferences diff --git a/app/ch12-forms/starter/pypi_org/data/downloads.py b/app/ch12-forms/starter/pypi_org/data/downloads.py index 66068f02..dd8afd65 100644 --- a/app/ch12-forms/starter/pypi_org/data/downloads.py +++ b/app/ch12-forms/starter/pypi_org/data/downloads.py @@ -7,8 +7,7 @@ class Download(SqlAlchemyBase): __tablename__ = 'downloads' id: int = sqlalchemy.Column(sqlalchemy.BigInteger, primary_key=True, autoincrement=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) package_id: str = sqlalchemy.Column(sqlalchemy.String, index=True) release_id: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) diff --git a/app/ch12-forms/starter/pypi_org/data/languages.py b/app/ch12-forms/starter/pypi_org/data/languages.py index 964b8444..3a73dde0 100644 --- a/app/ch12-forms/starter/pypi_org/data/languages.py +++ b/app/ch12-forms/starter/pypi_org/data/languages.py @@ -7,6 +7,5 @@ class ProgrammingLanguage(SqlAlchemyBase): __tablename__ = 'languages' id: str = sqlalchemy.Column(sqlalchemy.String, primary_key=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) description: str = sqlalchemy.Column(sqlalchemy.String) diff --git a/app/ch12-forms/starter/pypi_org/data/package.py b/app/ch12-forms/starter/pypi_org/data/package.py index c3420d1b..e0e48878 100644 --- a/app/ch12-forms/starter/pypi_org/data/package.py +++ b/app/ch12-forms/starter/pypi_org/data/package.py @@ -3,6 +3,8 @@ import sqlalchemy as sa import sqlalchemy.orm as orm +from sqlalchemy.orm import Mapped + from pypi_org.data.modelbase import SqlAlchemyBase from pypi_org.data.releases import Release @@ -12,7 +14,6 @@ class Package(SqlAlchemyBase): id: str = sa.Column(sa.String, primary_key=True) created_date: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) - last_updated: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) summary: str = sa.Column(sa.String, nullable=False) description: str = sa.Column(sa.String, nullable=True) @@ -26,11 +27,15 @@ class Package(SqlAlchemyBase): license: str = sa.Column(sa.String, index=True) # releases relationship - releases: List[Release] = orm.relation("Release", order_by=[ - Release.major_ver.desc(), - Release.minor_ver.desc(), - Release.build_ver.desc(), - ], back_populates='package') + releases: Mapped[Release] = orm.relationship( + 'Release', + order_by=[ + Release.major_ver.desc(), + Release.minor_ver.desc(), + Release.build_ver.desc(), + ], + back_populates='package', + ) def __repr__(self): return ''.format(self.id) diff --git a/app/ch12-forms/starter/pypi_org/data/releases.py b/app/ch12-forms/starter/pypi_org/data/releases.py index 7650e438..4b2db2bf 100644 --- a/app/ch12-forms/starter/pypi_org/data/releases.py +++ b/app/ch12-forms/starter/pypi_org/data/releases.py @@ -13,16 +13,14 @@ class Release(SqlAlchemyBase): minor_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) build_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) - created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, - default=datetime.datetime.now, - index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) comment: str = sqlalchemy.Column(sqlalchemy.String) url: str = sqlalchemy.Column(sqlalchemy.String) size: int = sqlalchemy.Column(sqlalchemy.BigInteger) # Package relationship - package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey("packages.id")) - package = orm.relation('Package') + package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey('packages.id')) + package = orm.relationship('Package') @property def version_text(self): diff --git a/app/ch12-forms/starter/pypi_org/infrastructure/num_convert.py b/app/ch12-forms/starter/pypi_org/infrastructure/num_convert.py new file mode 100644 index 00000000..4e1d8879 --- /dev/null +++ b/app/ch12-forms/starter/pypi_org/infrastructure/num_convert.py @@ -0,0 +1,8 @@ +from typing import Optional + + +def try_int(text) -> Optional[int]: + try: + return int(text) + except: + return None diff --git a/app/ch12-forms/starter/pypi_org/infrastructure/view_modifiers.py b/app/ch12-forms/starter/pypi_org/infrastructure/view_modifiers.py index 5bb6c178..d944c142 100644 --- a/app/ch12-forms/starter/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch12-forms/starter/pypi_org/infrastructure/view_modifiers.py @@ -1,6 +1,8 @@ from functools import wraps import flask +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -10,6 +12,10 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) + + if isinstance(response_val, werkzeug.wrappers.Response): + return response_val + if isinstance(response_val, flask.Response): return response_val @@ -20,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -36,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch12-forms/starter/pypi_org/services/package_service.py b/app/ch12-forms/starter/pypi_org/services/package_service.py index e51241ae..6ac445e6 100644 --- a/app/ch12-forms/starter/pypi_org/services/package_service.py +++ b/app/ch12-forms/starter/pypi_org/services/package_service.py @@ -9,11 +9,13 @@ def get_latest_releases(limit=10) -> List[Release]: session = db_session.create_session() - releases = session.query(Release). \ - options(sqlalchemy.orm.joinedload(Release.package)). \ - order_by(Release.created_date.desc()). \ - limit(limit). \ - all() + releases = ( + session.query(Release) + .options(sqlalchemy.orm.joinedload(Release.package)) + .order_by(Release.created_date.desc()) + .limit(limit) + .all() + ) session.close() @@ -38,10 +40,12 @@ def get_package_by_id(package_id: str) -> Optional[Package]: session = db_session.create_session() - package = session.query(Package) \ - .options(sqlalchemy.orm.joinedload(Package.releases)) \ - .filter(Package.id == package_id) \ + package = ( + session.query(Package) + .options(sqlalchemy.orm.joinedload(Package.releases)) + .filter(Package.id == package_id) .first() + ) session.close() diff --git a/app/ch12-forms/starter/pypi_org/views/account_views.py b/app/ch12-forms/starter/pypi_org/views/account_views.py index 0421324a..55e5df41 100644 --- a/app/ch12-forms/starter/pypi_org/views/account_views.py +++ b/app/ch12-forms/starter/pypi_org/views/account_views.py @@ -16,6 +16,7 @@ def index(): # ################### REGISTER ################################# + @blueprint.route('/account/register', methods=['GET']) @response(template_file='account/register.html') def register_get(): @@ -30,6 +31,7 @@ def register_post(): # ################### LOGIN ################################# + @blueprint.route('/account/login', methods=['GET']) @response(template_file='account/login.html') def login_get(): @@ -44,6 +46,7 @@ def login_post(): # ################### LOGOUT ################################# + @blueprint.route('/account/logout') def logout(): return {} diff --git a/app/ch12-forms/starter/pypi_org/views/cms_views.py b/app/ch12-forms/starter/pypi_org/views/cms_views.py index f7172d3d..a8b755a8 100644 --- a/app/ch12-forms/starter/pypi_org/views/cms_views.py +++ b/app/ch12-forms/starter/pypi_org/views/cms_views.py @@ -9,7 +9,7 @@ @blueprint.route('/') @response(template_file='cms/page.html') def cms_page(full_url: str): - print("Getting CMS page for {}".format(full_url)) + print('Getting CMS page for {}'.format(full_url)) page = cms_service.get_page(full_url) if not page: diff --git a/app/ch12-forms/starter/pypi_org/views/package_views.py b/app/ch12-forms/starter/pypi_org/views/package_views.py index 70526f59..4589fe56 100644 --- a/app/ch12-forms/starter/pypi_org/views/package_views.py +++ b/app/ch12-forms/starter/pypi_org/views/package_views.py @@ -16,12 +16,12 @@ def package_details(package_name: str): if not package: return flask.abort(status=404) - latest_version = "0.0.0" + latest_version = '0.0.0' latest_release = None is_latest = True if package.releases: - latest_release = package.releases[0] + latest_release = package.releases latest_version = latest_release.version_text return { @@ -36,4 +36,4 @@ def package_details(package_name: str): @blueprint.route('/') def popular(rank: int): print(type(rank), rank) - return "The details for the {}th most popular package".format(rank) + return 'The details for the {}th most popular package'.format(rank) diff --git a/app/ch12-forms/starter/requirements.piptools b/app/ch12-forms/starter/requirements.piptools new file mode 100644 index 00000000..5ba68752 --- /dev/null +++ b/app/ch12-forms/starter/requirements.piptools @@ -0,0 +1,5 @@ +alembic +flask +progressbar2 +python-dateutil +sqlalchemy diff --git a/app/ch12-forms/starter/requirements.txt b/app/ch12-forms/starter/requirements.txt index 9cf703d9..d802976d 100644 --- a/app/ch12-forms/starter/requirements.txt +++ b/app/ch12-forms/starter/requirements.txt @@ -1,6 +1,40 @@ -flask -sqlalchemy - -progressbar2 -python-dateutil - +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +alembic==1.14.1 + # via -r requirements.piptools +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +mako==1.3.9 + # via alembic +markupsafe==3.0.2 + # via + # jinja2 + # mako + # werkzeug +progressbar2==4.5.0 + # via -r requirements.piptools +python-dateutil==2.9.0.post0 + # via -r requirements.piptools +python-utils==3.9.1 + # via progressbar2 +six==1.17.0 + # via python-dateutil +sqlalchemy==2.0.38 + # via + # -r requirements.piptools + # alembic +typing-extensions==4.12.2 + # via + # alembic + # python-utils + # sqlalchemy +werkzeug==3.1.3 + # via flask diff --git a/app/ch13-validation/final/.idea/dataSources.local.xml b/app/ch13-validation/final/.idea/dataSources.local.xml index 94233bb2..7a996036 100644 --- a/app/ch13-validation/final/.idea/dataSources.local.xml +++ b/app/ch13-validation/final/.idea/dataSources.local.xml @@ -6,8 +6,12 @@ " - false - *:@ + no-auth + + + + + \ No newline at end of file diff --git a/app/ch13-validation/final/alembic/alembic_helpers.py b/app/ch13-validation/final/alembic/alembic_helpers.py index 561f7a70..6aea52d4 100644 --- a/app/ch13-validation/final/alembic/alembic_helpers.py +++ b/app/ch13-validation/final/alembic/alembic_helpers.py @@ -5,8 +5,7 @@ def table_has_column(table, column): config = op.get_context().config - engine = engine_from_config( - config.get_section(config.config_ini_section), prefix='sqlalchemy.') + engine = engine_from_config(config.get_section(config.config_ini_section), prefix='sqlalchemy.') insp = reflection.Inspector.from_engine(engine) has_column = False for col in insp.get_columns(table): diff --git a/app/ch13-validation/final/alembic/env.py b/app/ch13-validation/final/alembic/env.py index 27ec49fb..1fdc9cab 100644 --- a/app/ch13-validation/final/alembic/env.py +++ b/app/ch13-validation/final/alembic/env.py @@ -26,6 +26,7 @@ sys.path.insert(0, folder) from pypi_org.data.modelbase import SqlAlchemyBase + # noinspection PyUnresolvedReferences import pypi_org.data.__all_models @@ -49,10 +50,8 @@ def run_migrations_offline(): script output. """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, target_metadata=target_metadata, literal_binds=True - ) + url = config.get_main_option('sqlalchemy.url') + context.configure(url=url, target_metadata=target_metadata, literal_binds=True) with context.begin_transaction(): context.run_migrations() @@ -67,14 +66,12 @@ def run_migrations_online(): """ connectable = engine_from_config( config.get_section(config.config_ini_section), - prefix="sqlalchemy.", + prefix='sqlalchemy.', poolclass=pool.NullPool, ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() diff --git a/app/ch13-validation/final/alembic/versions/722c82f0097c_added_auditing_table.py b/app/ch13-validation/final/alembic/versions/722c82f0097c_added_auditing_table.py index 2c3e2a39..7c6e892f 100644 --- a/app/ch13-validation/final/alembic/versions/722c82f0097c_added_auditing_table.py +++ b/app/ch13-validation/final/alembic/versions/722c82f0097c_added_auditing_table.py @@ -18,11 +18,12 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('auditing', - sa.Column('id', sa.String(), nullable=False), - sa.Column('created_date', sa.DateTime(), nullable=True), - sa.Column('description', sa.String(), nullable=True), - sa.PrimaryKeyConstraint('id') + op.create_table( + 'auditing', + sa.Column('id', sa.String(), nullable=False), + sa.Column('created_date', sa.DateTime(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id'), ) op.create_index(op.f('ix_auditing_created_date'), 'auditing', ['created_date'], unique=False) # ### end Alembic commands ### diff --git a/app/ch13-validation/final/pypi_org/app.py b/app/ch13-validation/final/pypi_org/app.py index 89004d09..9b491bad 100644 --- a/app/ch13-validation/final/pypi_org/app.py +++ b/app/ch13-validation/final/pypi_org/app.py @@ -1,6 +1,7 @@ import os import sys import flask + folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, folder) @@ -16,10 +17,7 @@ def main(): def setup_db(): - db_file = os.path.join( - os.path.dirname(__file__), - 'db', - 'pypi.sqlite') + db_file = os.path.join(os.path.dirname(__file__), 'db', 'pypi.sqlite') db_session.global_init(db_file) diff --git a/app/ch13-validation/final/pypi_org/bin/basic_inserts.py b/app/ch13-validation/final/pypi_org/bin/basic_inserts.py index 3a2f360d..6d04f551 100644 --- a/app/ch13-validation/final/pypi_org/bin/basic_inserts.py +++ b/app/ch13-validation/final/pypi_org/bin/basic_inserts.py @@ -1,14 +1,13 @@ import os import sys +# Make it run more easily outside of PyCharm +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + import pypi_org.data.db_session as db_session from pypi_org.data.package import Package from pypi_org.data.releases import Release -# Make it run more easily outside of PyCharm -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) - def main(): init_db() @@ -20,24 +19,24 @@ def insert_a_package(): p = Package() p.id = input('Package id / name: ').strip().lower() - p.summary = input("Package summary: ").strip() - p.author_name = input("Author: ").strip() - p.license = input("License: ").strip() + p.summary = input('Package summary: ').strip() + p.author_name = input('Author: ').strip() + p.license = input('License: ').strip() - print("Release 1:") + print('Release 1:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) - print("Release 2:") + print('Release 2:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) session = db_session.create_session() diff --git a/app/ch13-validation/final/pypi_org/bin/load_data.py b/app/ch13-validation/final/pypi_org/bin/load_data.py index 15237f18..3af7a03d 100644 --- a/app/ch13-validation/final/pypi_org/bin/load_data.py +++ b/app/ch13-validation/final/pypi_org/bin/load_data.py @@ -7,9 +7,9 @@ import progressbar from dateutil.parser import parse -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) +from pypi_org.infrastructure.num_convert import try_int import pypi_org.data.db_session as db_session from pypi_org.data.languages import ProgrammingLanguage from pypi_org.data.licenses import License @@ -39,7 +39,7 @@ def main(): def do_import_languages(file_data: List[dict]): imported = set() - print("Importing languages ... ", flush=True) + print('Importing languages ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -74,7 +74,7 @@ def do_import_languages(file_data: List[dict]): def do_import_licenses(file_data: List[dict]): imported = set() - print("Importing licenses ... ", flush=True) + print('Importing licenses ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -100,17 +100,17 @@ def do_import_licenses(file_data: List[dict]): def do_summary(): session = db_session.create_session() - print("Final numbers:") - print("Users: {:,}".format(session.query(User).count())) - print("Packages: {:,}".format(session.query(Package).count())) - print("Releases: {:,}".format(session.query(Release).count())) - print("Maintainers: {:,}".format(session.query(Maintainer).count())) - print("Languages: {:,}".format(session.query(ProgrammingLanguage).count())) - print("Licenses: {:,}".format(session.query(License).count())) + print('Final numbers:') + print('Users: {:,}'.format(session.query(User).count())) + print('Packages: {:,}'.format(session.query(Package).count())) + print('Releases: {:,}'.format(session.query(Release).count())) + print('Maintainers: {:,}'.format(session.query(Maintainer).count())) + print('Languages: {:,}'.format(session.query(ProgrammingLanguage).count())) + print('Licenses: {:,}'.format(session.query(License).count())) def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: - print("Importing users ... ", flush=True) + print('Importing users ... ', flush=True) with progressbar.ProgressBar(max_value=len(user_lookup)) as bar: for idx, (email, name) in enumerate(user_lookup.items()): session = db_session.create_session() @@ -134,29 +134,29 @@ def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: def do_import_packages(file_data: List[dict], user_lookup: Dict[str, User]): errored_packages = [] - print("Importing packages and releases ... ", flush=True) + print('Importing packages and releases ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): try: load_package(p, user_lookup) bar.update(idx) except Exception as x: - errored_packages.append((p, " *** Errored out for package {}, {}".format(p.get('package_name'), x))) + errored_packages.append((p, ' *** Errored out for package {}, {}'.format(p.get('package_name'), x))) raise sys.stderr.flush() sys.stdout.flush() print() - print("Completed packages with {} errors.".format(len(errored_packages))) - for (p, txt) in errored_packages: + print('Completed packages with {} errors.'.format(len(errored_packages))) + for p, txt in errored_packages: print(txt) def do_load_files() -> List[dict]: data_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../../data/pypi-top-100')) - print("Loading files from {}".format(data_path)) + print('Loading files from {}'.format(data_path)) files = get_file_names(data_path) - print("Found {:,} files, loading ...".format(len(files)), flush=True) - time.sleep(.1) + print('Found {:,} files, loading ...'.format(len(files)), flush=True) + time.sleep(0.1) file_data = [] with progressbar.ProgressBar(max_value=len(files)) as bar: @@ -171,7 +171,7 @@ def do_load_files() -> List[dict]: def find_users(data: List[dict]) -> dict: - print("Discovering users...", flush=True) + print('Discovering users...', flush=True) found_users = {} with progressbar.ProgressBar(max_value=len(data)) as bar: @@ -184,7 +184,7 @@ def find_users(data: List[dict]) -> dict: sys.stderr.flush() sys.stdout.flush() print() - print("Discovered {:,} users".format(len(found_users))) + print('Discovered {:,} users'.format(len(found_users))) print() return found_users @@ -215,7 +215,7 @@ def load_file_data(filename: str) -> dict: with open(filename, 'r', encoding='utf-8') as fin: data = json.load(fin) except Exception as x: - print("ERROR in file: {}, details: {}".format(filename, x), flush=True) + print('ERROR in file: {}, details: {}'.format(filename, x), flush=True) raise return data @@ -233,7 +233,7 @@ def load_package(data: dict, user_lookup: Dict[str, User]): p.author = info.get('author') p.author_email = info.get('author_email') - releases = build_releases(p.id, data.get("releases", {})) + releases = build_releases(p.id, data.get('releases', {})) if releases: p.created_date = releases[0].created_date @@ -283,18 +283,13 @@ def detect_license(license_text: str) -> Optional[str]: license_text = license_text.strip() if len(license_text) > 100 or '\n' in license_text: - return "CUSTOM" + return 'CUSTOM' - license_text = license_text \ - .replace('Software License', '') \ - .replace('License', '') + license_text = license_text.replace('Software License', '').replace('License', '') if '::' in license_text: # E.g. 'License :: OSI Approved :: Apache Software License' - return license_text \ - .split(':')[-1] \ - .replace(' ', ' ') \ - .strip() + return license_text.split(':')[-1].replace(' ', ' ').strip() return license_text.strip() @@ -339,13 +334,6 @@ def make_version_num(version_text): return major, minor, build -def try_int(text) -> int: - try: - return int(text) - except: - return 0 - - def init_db(): top_folder = os.path.dirname(__file__) rel_file = os.path.join('..', 'db', 'pypi.sqlite') @@ -357,9 +345,7 @@ def get_file_names(data_path: str) -> List[str]: files = [] for f in os.listdir(data_path): if f.endswith('.json'): - files.append( - os.path.abspath(os.path.join(data_path, f)) - ) + files.append(os.path.abspath(os.path.join(data_path, f))) files.sort() return files diff --git a/app/ch13-validation/final/pypi_org/data/__all_models.py b/app/ch13-validation/final/pypi_org/data/__all_models.py index f399ef54..ca4804ac 100644 --- a/app/ch13-validation/final/pypi_org/data/__all_models.py +++ b/app/ch13-validation/final/pypi_org/data/__all_models.py @@ -5,17 +5,24 @@ # noinspection PyUnresolvedReferences import pypi_org.data.audit + # noinspection PyUnresolvedReferences import pypi_org.data.downloads + # noinspection PyUnresolvedReferences import pypi_org.data.languages + # noinspection PyUnresolvedReferences import pypi_org.data.licenses + # noinspection PyUnresolvedReferences import pypi_org.data.maintainers + # noinspection PyUnresolvedReferences import pypi_org.data.package + # noinspection PyUnresolvedReferences import pypi_org.data.releases + # noinspection PyUnresolvedReferences import pypi_org.data.users diff --git a/app/ch13-validation/final/pypi_org/data/db_session.py b/app/ch13-validation/final/pypi_org/data/db_session.py index acfc485d..86eb3fb2 100644 --- a/app/ch13-validation/final/pypi_org/data/db_session.py +++ b/app/ch13-validation/final/pypi_org/data/db_session.py @@ -14,12 +14,15 @@ def global_init(db_file: str): return if not db_file or not db_file.strip(): - raise Exception("You must specify a db file.") + raise Exception('You must specify a db file.') conn_str = 'sqlite:///' + db_file.strip() - print("Connecting to DB with {}".format(conn_str)) + print('Connecting to DB with {}'.format(conn_str)) - engine = sa.create_engine(conn_str, echo=False) + # Adding check_same_thread = False after the recording. This can be an issue about + # creating / owner thread when cleaning up sessions, etc. This is a sqlite restriction + # that we probably don't care about in this example. + engine = sa.create_engine(conn_str, echo=False, connect_args={'check_same_thread': False}) __factory = orm.sessionmaker(bind=engine) # noinspection PyUnresolvedReferences diff --git a/app/ch13-validation/final/pypi_org/data/downloads.py b/app/ch13-validation/final/pypi_org/data/downloads.py index 66068f02..dd8afd65 100644 --- a/app/ch13-validation/final/pypi_org/data/downloads.py +++ b/app/ch13-validation/final/pypi_org/data/downloads.py @@ -7,8 +7,7 @@ class Download(SqlAlchemyBase): __tablename__ = 'downloads' id: int = sqlalchemy.Column(sqlalchemy.BigInteger, primary_key=True, autoincrement=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) package_id: str = sqlalchemy.Column(sqlalchemy.String, index=True) release_id: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) diff --git a/app/ch13-validation/final/pypi_org/data/languages.py b/app/ch13-validation/final/pypi_org/data/languages.py index 964b8444..3a73dde0 100644 --- a/app/ch13-validation/final/pypi_org/data/languages.py +++ b/app/ch13-validation/final/pypi_org/data/languages.py @@ -7,6 +7,5 @@ class ProgrammingLanguage(SqlAlchemyBase): __tablename__ = 'languages' id: str = sqlalchemy.Column(sqlalchemy.String, primary_key=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) description: str = sqlalchemy.Column(sqlalchemy.String) diff --git a/app/ch13-validation/final/pypi_org/data/package.py b/app/ch13-validation/final/pypi_org/data/package.py index c3420d1b..e0e48878 100644 --- a/app/ch13-validation/final/pypi_org/data/package.py +++ b/app/ch13-validation/final/pypi_org/data/package.py @@ -3,6 +3,8 @@ import sqlalchemy as sa import sqlalchemy.orm as orm +from sqlalchemy.orm import Mapped + from pypi_org.data.modelbase import SqlAlchemyBase from pypi_org.data.releases import Release @@ -12,7 +14,6 @@ class Package(SqlAlchemyBase): id: str = sa.Column(sa.String, primary_key=True) created_date: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) - last_updated: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) summary: str = sa.Column(sa.String, nullable=False) description: str = sa.Column(sa.String, nullable=True) @@ -26,11 +27,15 @@ class Package(SqlAlchemyBase): license: str = sa.Column(sa.String, index=True) # releases relationship - releases: List[Release] = orm.relation("Release", order_by=[ - Release.major_ver.desc(), - Release.minor_ver.desc(), - Release.build_ver.desc(), - ], back_populates='package') + releases: Mapped[Release] = orm.relationship( + 'Release', + order_by=[ + Release.major_ver.desc(), + Release.minor_ver.desc(), + Release.build_ver.desc(), + ], + back_populates='package', + ) def __repr__(self): return ''.format(self.id) diff --git a/app/ch13-validation/final/pypi_org/data/releases.py b/app/ch13-validation/final/pypi_org/data/releases.py index 7650e438..4b2db2bf 100644 --- a/app/ch13-validation/final/pypi_org/data/releases.py +++ b/app/ch13-validation/final/pypi_org/data/releases.py @@ -13,16 +13,14 @@ class Release(SqlAlchemyBase): minor_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) build_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) - created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, - default=datetime.datetime.now, - index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) comment: str = sqlalchemy.Column(sqlalchemy.String) url: str = sqlalchemy.Column(sqlalchemy.String) size: int = sqlalchemy.Column(sqlalchemy.BigInteger) # Package relationship - package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey("packages.id")) - package = orm.relation('Package') + package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey('packages.id')) + package = orm.relationship('Package') @property def version_text(self): diff --git a/app/ch13-validation/final/pypi_org/infrastructure/cookie_auth.py b/app/ch13-validation/final/pypi_org/infrastructure/cookie_auth.py index 64dc85a7..c215c0af 100644 --- a/app/ch13-validation/final/pypi_org/infrastructure/cookie_auth.py +++ b/app/ch13-validation/final/pypi_org/infrastructure/cookie_auth.py @@ -1,19 +1,18 @@ import hashlib -from datetime import timedelta from typing import Optional from flask import Request from flask import Response -from pypi_org.bin.load_data import try_int +from pypi_org.infrastructure.num_convert import try_int auth_cookie_name = 'pypi_demo_user' def set_auth(response: Response, user_id: int): hash_val = __hash_text(str(user_id)) - val = "{}:{}".format(user_id, hash_val) - response.set_cookie(auth_cookie_name, val) + val = '{}:{}'.format(user_id, hash_val) + response.set_cookie(auth_cookie_name, val, secure=False, httponly=True, samesite='Lax') def __hash_text(text: str) -> str: @@ -21,10 +20,6 @@ def __hash_text(text: str) -> str: return hashlib.sha512(text.encode('utf-8')).hexdigest() -def __add_cookie_callback(_, response: Response, name: str, value: str): - response.set_cookie(name, value, max_age=timedelta(days=30)) - - def get_user_id_via_auth_cookie(request: Request) -> Optional[int]: if auth_cookie_name not in request.cookies: return None @@ -38,7 +33,7 @@ def get_user_id_via_auth_cookie(request: Request) -> Optional[int]: hash_val = parts[1] hash_val_check = __hash_text(user_id) if hash_val != hash_val_check: - print("Warning: Hash mismatch, invalid cookie value") + print('Warning: Hash mismatch, invalid cookie value') return None return try_int(user_id) diff --git a/app/ch13-validation/final/pypi_org/infrastructure/num_convert.py b/app/ch13-validation/final/pypi_org/infrastructure/num_convert.py new file mode 100644 index 00000000..4e1d8879 --- /dev/null +++ b/app/ch13-validation/final/pypi_org/infrastructure/num_convert.py @@ -0,0 +1,8 @@ +from typing import Optional + + +def try_int(text) -> Optional[int]: + try: + return int(text) + except: + return None diff --git a/app/ch13-validation/final/pypi_org/infrastructure/request_dict.py b/app/ch13-validation/final/pypi_org/infrastructure/request_dict.py index 495fd979..d44ae574 100644 --- a/app/ch13-validation/final/pypi_org/infrastructure/request_dict.py +++ b/app/ch13-validation/final/pypi_org/infrastructure/request_dict.py @@ -1,4 +1,5 @@ import flask +from werkzeug.datastructures import MultiDict class RequestDictionary(dict): @@ -13,11 +14,22 @@ def __getattr__(self, key): def create(default_val=None, **route_args) -> RequestDictionary: request = flask.request + # Adding this retro actively. Some folks are experiencing issues where they + # are getting a list rather than plain dict. I think it's from multiple + # entries in the multidict. This should fix it. + args = request.args + if isinstance(request.args, MultiDict): + args = request.args.to_dict() + + form = request.form + if isinstance(request.args, MultiDict): + form = request.form.to_dict() + data = { - **request.args, # The key/value pairs in the URL query string + **args, # The key/value pairs in the URL query string **request.headers, # Header values - **request.form, # The key/value pairs in the body, from a HTML post form - **route_args # And additional arguments the method access, if they want them merged. + **form, # The key/value pairs in the body, from a HTML post form + **route_args, # And additional arguments the method access, if they want them merged. } return RequestDictionary(data, default_val=default_val) diff --git a/app/ch13-validation/final/pypi_org/infrastructure/view_modifiers.py b/app/ch13-validation/final/pypi_org/infrastructure/view_modifiers.py index 82ce5baf..d944c142 100644 --- a/app/ch13-validation/final/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch13-validation/final/pypi_org/infrastructure/view_modifiers.py @@ -1,7 +1,8 @@ from functools import wraps import flask -import werkzeug.wrappers.response +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -11,9 +12,11 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) - if isinstance(response_val, flask.Response): + + if isinstance(response_val, werkzeug.wrappers.Response): return response_val - if isinstance(response_val, werkzeug.wrappers.response.Response): + + if isinstance(response_val, flask.Response): return response_val if isinstance(response_val, dict): @@ -23,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -39,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch13-validation/final/pypi_org/services/package_service.py b/app/ch13-validation/final/pypi_org/services/package_service.py index 71d4d02f..c2675f8d 100644 --- a/app/ch13-validation/final/pypi_org/services/package_service.py +++ b/app/ch13-validation/final/pypi_org/services/package_service.py @@ -9,12 +9,13 @@ def get_latest_releases(limit=10) -> List[Release]: session = db_session.create_session() try: - - releases = session.query(Release). \ - options(sqlalchemy.orm.joinedload(Release.package)). \ - order_by(Release.created_date.desc()). \ - limit(limit). \ - all() + releases = ( + session.query(Release) + .options(sqlalchemy.orm.joinedload(Release.package)) + .order_by(Release.created_date.desc()) + .limit(limit) + .all() + ) finally: session.close() @@ -46,11 +47,12 @@ def get_package_by_id(package_id: str) -> Optional[Package]: session = db_session.create_session() try: - - package = session.query(Package) \ - .options(sqlalchemy.orm.joinedload(Package.releases)) \ - .filter(Package.id == package_id) \ + package = ( + session.query(Package) + .options(sqlalchemy.orm.joinedload(Package.releases)) + .filter(Package.id == package_id) .first() + ) finally: session.close() diff --git a/app/ch13-validation/final/pypi_org/viewmodels/cms/page_viewmodel.py b/app/ch13-validation/final/pypi_org/viewmodels/cms/page_viewmodel.py index b7bb3249..ec033c3f 100644 --- a/app/ch13-validation/final/pypi_org/viewmodels/cms/page_viewmodel.py +++ b/app/ch13-validation/final/pypi_org/viewmodels/cms/page_viewmodel.py @@ -7,4 +7,3 @@ def __init__(self, full_url: str): super().__init__() self.page = cms_service.get_page(full_url) - diff --git a/app/ch13-validation/final/pypi_org/viewmodels/packages/pagedetails_viewmodel.py b/app/ch13-validation/final/pypi_org/viewmodels/packages/pagedetails_viewmodel.py index 63c4cf68..62bdca3d 100644 --- a/app/ch13-validation/final/pypi_org/viewmodels/packages/pagedetails_viewmodel.py +++ b/app/ch13-validation/final/pypi_org/viewmodels/packages/pagedetails_viewmodel.py @@ -11,12 +11,12 @@ def __init__(self, package_name: str): self.package_name = package_name.strip().lower() self.package = package_service.get_package_by_id(self.package_name) - self.latest_version = "0.0.0" + self.latest_version = '0.0.0' self.latest_release = None self.is_latest = True if self.package and self.package.releases: - self.latest_release = self.package.releases[0] + self.latest_release = self.package.releases self.latest_version = self.latest_release.version_text self.release_version = self.latest_release diff --git a/app/ch13-validation/final/pypi_org/views/account_views.py b/app/ch13-validation/final/pypi_org/views/account_views.py index 8697cc24..83df8950 100644 --- a/app/ch13-validation/final/pypi_org/views/account_views.py +++ b/app/ch13-validation/final/pypi_org/views/account_views.py @@ -25,6 +25,7 @@ def index(): # ################### REGISTER ################################# + @blueprint.route('/account/register', methods=['GET']) @response(template_file='account/register.html') def register_get(): @@ -54,6 +55,7 @@ def register_post(): # ################### LOGIN ################################# + @blueprint.route('/account/login', methods=['GET']) @response(template_file='account/login.html') def login_get(): @@ -72,7 +74,7 @@ def login_post(): user = user_service.login_user(vm.email, vm.password) if not user: - vm.error = "The account does not exist or the password is wrong." + vm.error = 'The account does not exist or the password is wrong.' return vm.to_dict() resp = flask.redirect('/account') @@ -83,6 +85,7 @@ def login_post(): # ################### LOGOUT ################################# + @blueprint.route('/account/logout') def logout(): resp = flask.redirect('/') diff --git a/app/ch13-validation/final/pypi_org/views/package_views.py b/app/ch13-validation/final/pypi_org/views/package_views.py index 1a74549f..2f862740 100644 --- a/app/ch13-validation/final/pypi_org/views/package_views.py +++ b/app/ch13-validation/final/pypi_org/views/package_views.py @@ -19,4 +19,4 @@ def package_details(package_name: str): @blueprint.route('/') def popular(rank: int): print(type(rank), rank) - return "The details for the {}th most popular package".format(rank) + return 'The details for the {}th most popular package'.format(rank) diff --git a/app/ch13-validation/final/requirements.piptools b/app/ch13-validation/final/requirements.piptools new file mode 100644 index 00000000..d13bf0c8 --- /dev/null +++ b/app/ch13-validation/final/requirements.piptools @@ -0,0 +1,6 @@ +alembic +flask +passlib +progressbar2 +python-dateutil +sqlalchemy diff --git a/app/ch13-validation/final/requirements.txt b/app/ch13-validation/final/requirements.txt index e10973d8..19fc8c45 100644 --- a/app/ch13-validation/final/requirements.txt +++ b/app/ch13-validation/final/requirements.txt @@ -1,8 +1,42 @@ -werkzeug -flask -sqlalchemy - -progressbar2 -python-dateutil -passlib - +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +alembic==1.14.1 + # via -r requirements.piptools +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +mako==1.3.9 + # via alembic +markupsafe==3.0.2 + # via + # jinja2 + # mako + # werkzeug +passlib==1.7.4 + # via -r requirements.piptools +progressbar2==4.5.0 + # via -r requirements.piptools +python-dateutil==2.9.0.post0 + # via -r requirements.piptools +python-utils==3.9.1 + # via progressbar2 +six==1.17.0 + # via python-dateutil +sqlalchemy==2.0.38 + # via + # -r requirements.piptools + # alembic +typing-extensions==4.12.2 + # via + # alembic + # python-utils + # sqlalchemy +werkzeug==3.1.3 + # via flask diff --git a/app/ch13-validation/starter/.idea/dataSources.local.xml b/app/ch13-validation/starter/.idea/dataSources.local.xml index 94233bb2..7a996036 100644 --- a/app/ch13-validation/starter/.idea/dataSources.local.xml +++ b/app/ch13-validation/starter/.idea/dataSources.local.xml @@ -6,8 +6,12 @@ " - false - *:@ + no-auth + + + + + \ No newline at end of file diff --git a/app/ch13-validation/starter/alembic/alembic_helpers.py b/app/ch13-validation/starter/alembic/alembic_helpers.py index 561f7a70..6aea52d4 100644 --- a/app/ch13-validation/starter/alembic/alembic_helpers.py +++ b/app/ch13-validation/starter/alembic/alembic_helpers.py @@ -5,8 +5,7 @@ def table_has_column(table, column): config = op.get_context().config - engine = engine_from_config( - config.get_section(config.config_ini_section), prefix='sqlalchemy.') + engine = engine_from_config(config.get_section(config.config_ini_section), prefix='sqlalchemy.') insp = reflection.Inspector.from_engine(engine) has_column = False for col in insp.get_columns(table): diff --git a/app/ch13-validation/starter/alembic/env.py b/app/ch13-validation/starter/alembic/env.py index 27ec49fb..1fdc9cab 100644 --- a/app/ch13-validation/starter/alembic/env.py +++ b/app/ch13-validation/starter/alembic/env.py @@ -26,6 +26,7 @@ sys.path.insert(0, folder) from pypi_org.data.modelbase import SqlAlchemyBase + # noinspection PyUnresolvedReferences import pypi_org.data.__all_models @@ -49,10 +50,8 @@ def run_migrations_offline(): script output. """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, target_metadata=target_metadata, literal_binds=True - ) + url = config.get_main_option('sqlalchemy.url') + context.configure(url=url, target_metadata=target_metadata, literal_binds=True) with context.begin_transaction(): context.run_migrations() @@ -67,14 +66,12 @@ def run_migrations_online(): """ connectable = engine_from_config( config.get_section(config.config_ini_section), - prefix="sqlalchemy.", + prefix='sqlalchemy.', poolclass=pool.NullPool, ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() diff --git a/app/ch13-validation/starter/alembic/versions/722c82f0097c_added_auditing_table.py b/app/ch13-validation/starter/alembic/versions/722c82f0097c_added_auditing_table.py index 2c3e2a39..7c6e892f 100644 --- a/app/ch13-validation/starter/alembic/versions/722c82f0097c_added_auditing_table.py +++ b/app/ch13-validation/starter/alembic/versions/722c82f0097c_added_auditing_table.py @@ -18,11 +18,12 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('auditing', - sa.Column('id', sa.String(), nullable=False), - sa.Column('created_date', sa.DateTime(), nullable=True), - sa.Column('description', sa.String(), nullable=True), - sa.PrimaryKeyConstraint('id') + op.create_table( + 'auditing', + sa.Column('id', sa.String(), nullable=False), + sa.Column('created_date', sa.DateTime(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id'), ) op.create_index(op.f('ix_auditing_created_date'), 'auditing', ['created_date'], unique=False) # ### end Alembic commands ### diff --git a/app/ch13-validation/starter/pypi_org/app.py b/app/ch13-validation/starter/pypi_org/app.py index 89004d09..9b491bad 100644 --- a/app/ch13-validation/starter/pypi_org/app.py +++ b/app/ch13-validation/starter/pypi_org/app.py @@ -1,6 +1,7 @@ import os import sys import flask + folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, folder) @@ -16,10 +17,7 @@ def main(): def setup_db(): - db_file = os.path.join( - os.path.dirname(__file__), - 'db', - 'pypi.sqlite') + db_file = os.path.join(os.path.dirname(__file__), 'db', 'pypi.sqlite') db_session.global_init(db_file) diff --git a/app/ch13-validation/starter/pypi_org/bin/basic_inserts.py b/app/ch13-validation/starter/pypi_org/bin/basic_inserts.py index 3a2f360d..6d04f551 100644 --- a/app/ch13-validation/starter/pypi_org/bin/basic_inserts.py +++ b/app/ch13-validation/starter/pypi_org/bin/basic_inserts.py @@ -1,14 +1,13 @@ import os import sys +# Make it run more easily outside of PyCharm +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + import pypi_org.data.db_session as db_session from pypi_org.data.package import Package from pypi_org.data.releases import Release -# Make it run more easily outside of PyCharm -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) - def main(): init_db() @@ -20,24 +19,24 @@ def insert_a_package(): p = Package() p.id = input('Package id / name: ').strip().lower() - p.summary = input("Package summary: ").strip() - p.author_name = input("Author: ").strip() - p.license = input("License: ").strip() + p.summary = input('Package summary: ').strip() + p.author_name = input('Author: ').strip() + p.license = input('License: ').strip() - print("Release 1:") + print('Release 1:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) - print("Release 2:") + print('Release 2:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) session = db_session.create_session() diff --git a/app/ch13-validation/starter/pypi_org/bin/load_data.py b/app/ch13-validation/starter/pypi_org/bin/load_data.py index 15237f18..3af7a03d 100644 --- a/app/ch13-validation/starter/pypi_org/bin/load_data.py +++ b/app/ch13-validation/starter/pypi_org/bin/load_data.py @@ -7,9 +7,9 @@ import progressbar from dateutil.parser import parse -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) +from pypi_org.infrastructure.num_convert import try_int import pypi_org.data.db_session as db_session from pypi_org.data.languages import ProgrammingLanguage from pypi_org.data.licenses import License @@ -39,7 +39,7 @@ def main(): def do_import_languages(file_data: List[dict]): imported = set() - print("Importing languages ... ", flush=True) + print('Importing languages ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -74,7 +74,7 @@ def do_import_languages(file_data: List[dict]): def do_import_licenses(file_data: List[dict]): imported = set() - print("Importing licenses ... ", flush=True) + print('Importing licenses ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -100,17 +100,17 @@ def do_import_licenses(file_data: List[dict]): def do_summary(): session = db_session.create_session() - print("Final numbers:") - print("Users: {:,}".format(session.query(User).count())) - print("Packages: {:,}".format(session.query(Package).count())) - print("Releases: {:,}".format(session.query(Release).count())) - print("Maintainers: {:,}".format(session.query(Maintainer).count())) - print("Languages: {:,}".format(session.query(ProgrammingLanguage).count())) - print("Licenses: {:,}".format(session.query(License).count())) + print('Final numbers:') + print('Users: {:,}'.format(session.query(User).count())) + print('Packages: {:,}'.format(session.query(Package).count())) + print('Releases: {:,}'.format(session.query(Release).count())) + print('Maintainers: {:,}'.format(session.query(Maintainer).count())) + print('Languages: {:,}'.format(session.query(ProgrammingLanguage).count())) + print('Licenses: {:,}'.format(session.query(License).count())) def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: - print("Importing users ... ", flush=True) + print('Importing users ... ', flush=True) with progressbar.ProgressBar(max_value=len(user_lookup)) as bar: for idx, (email, name) in enumerate(user_lookup.items()): session = db_session.create_session() @@ -134,29 +134,29 @@ def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: def do_import_packages(file_data: List[dict], user_lookup: Dict[str, User]): errored_packages = [] - print("Importing packages and releases ... ", flush=True) + print('Importing packages and releases ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): try: load_package(p, user_lookup) bar.update(idx) except Exception as x: - errored_packages.append((p, " *** Errored out for package {}, {}".format(p.get('package_name'), x))) + errored_packages.append((p, ' *** Errored out for package {}, {}'.format(p.get('package_name'), x))) raise sys.stderr.flush() sys.stdout.flush() print() - print("Completed packages with {} errors.".format(len(errored_packages))) - for (p, txt) in errored_packages: + print('Completed packages with {} errors.'.format(len(errored_packages))) + for p, txt in errored_packages: print(txt) def do_load_files() -> List[dict]: data_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../../data/pypi-top-100')) - print("Loading files from {}".format(data_path)) + print('Loading files from {}'.format(data_path)) files = get_file_names(data_path) - print("Found {:,} files, loading ...".format(len(files)), flush=True) - time.sleep(.1) + print('Found {:,} files, loading ...'.format(len(files)), flush=True) + time.sleep(0.1) file_data = [] with progressbar.ProgressBar(max_value=len(files)) as bar: @@ -171,7 +171,7 @@ def do_load_files() -> List[dict]: def find_users(data: List[dict]) -> dict: - print("Discovering users...", flush=True) + print('Discovering users...', flush=True) found_users = {} with progressbar.ProgressBar(max_value=len(data)) as bar: @@ -184,7 +184,7 @@ def find_users(data: List[dict]) -> dict: sys.stderr.flush() sys.stdout.flush() print() - print("Discovered {:,} users".format(len(found_users))) + print('Discovered {:,} users'.format(len(found_users))) print() return found_users @@ -215,7 +215,7 @@ def load_file_data(filename: str) -> dict: with open(filename, 'r', encoding='utf-8') as fin: data = json.load(fin) except Exception as x: - print("ERROR in file: {}, details: {}".format(filename, x), flush=True) + print('ERROR in file: {}, details: {}'.format(filename, x), flush=True) raise return data @@ -233,7 +233,7 @@ def load_package(data: dict, user_lookup: Dict[str, User]): p.author = info.get('author') p.author_email = info.get('author_email') - releases = build_releases(p.id, data.get("releases", {})) + releases = build_releases(p.id, data.get('releases', {})) if releases: p.created_date = releases[0].created_date @@ -283,18 +283,13 @@ def detect_license(license_text: str) -> Optional[str]: license_text = license_text.strip() if len(license_text) > 100 or '\n' in license_text: - return "CUSTOM" + return 'CUSTOM' - license_text = license_text \ - .replace('Software License', '') \ - .replace('License', '') + license_text = license_text.replace('Software License', '').replace('License', '') if '::' in license_text: # E.g. 'License :: OSI Approved :: Apache Software License' - return license_text \ - .split(':')[-1] \ - .replace(' ', ' ') \ - .strip() + return license_text.split(':')[-1].replace(' ', ' ').strip() return license_text.strip() @@ -339,13 +334,6 @@ def make_version_num(version_text): return major, minor, build -def try_int(text) -> int: - try: - return int(text) - except: - return 0 - - def init_db(): top_folder = os.path.dirname(__file__) rel_file = os.path.join('..', 'db', 'pypi.sqlite') @@ -357,9 +345,7 @@ def get_file_names(data_path: str) -> List[str]: files = [] for f in os.listdir(data_path): if f.endswith('.json'): - files.append( - os.path.abspath(os.path.join(data_path, f)) - ) + files.append(os.path.abspath(os.path.join(data_path, f))) files.sort() return files diff --git a/app/ch13-validation/starter/pypi_org/data/__all_models.py b/app/ch13-validation/starter/pypi_org/data/__all_models.py index f399ef54..ca4804ac 100644 --- a/app/ch13-validation/starter/pypi_org/data/__all_models.py +++ b/app/ch13-validation/starter/pypi_org/data/__all_models.py @@ -5,17 +5,24 @@ # noinspection PyUnresolvedReferences import pypi_org.data.audit + # noinspection PyUnresolvedReferences import pypi_org.data.downloads + # noinspection PyUnresolvedReferences import pypi_org.data.languages + # noinspection PyUnresolvedReferences import pypi_org.data.licenses + # noinspection PyUnresolvedReferences import pypi_org.data.maintainers + # noinspection PyUnresolvedReferences import pypi_org.data.package + # noinspection PyUnresolvedReferences import pypi_org.data.releases + # noinspection PyUnresolvedReferences import pypi_org.data.users diff --git a/app/ch13-validation/starter/pypi_org/data/db_session.py b/app/ch13-validation/starter/pypi_org/data/db_session.py index 50861b3b..62391b85 100644 --- a/app/ch13-validation/starter/pypi_org/data/db_session.py +++ b/app/ch13-validation/starter/pypi_org/data/db_session.py @@ -14,12 +14,15 @@ def global_init(db_file: str): return if not db_file or not db_file.strip(): - raise Exception("You must specify a db file.") + raise Exception('You must specify a db file.') conn_str = 'sqlite:///' + db_file.strip() - print("Connecting to DB with {}".format(conn_str)) + print('Connecting to DB with {}'.format(conn_str)) - engine = sa.create_engine(conn_str, echo=False) + # Adding check_same_thread = False after the recording. This can be an issue about + # creating / owner thread when cleaning up sessions, etc. This is a sqlite restriction + # that we probably don't care about in this example. + engine = sa.create_engine(conn_str, echo=False, connect_args={'check_same_thread': False}) __factory = orm.sessionmaker(bind=engine) # noinspection PyUnresolvedReferences diff --git a/app/ch13-validation/starter/pypi_org/data/downloads.py b/app/ch13-validation/starter/pypi_org/data/downloads.py index 66068f02..dd8afd65 100644 --- a/app/ch13-validation/starter/pypi_org/data/downloads.py +++ b/app/ch13-validation/starter/pypi_org/data/downloads.py @@ -7,8 +7,7 @@ class Download(SqlAlchemyBase): __tablename__ = 'downloads' id: int = sqlalchemy.Column(sqlalchemy.BigInteger, primary_key=True, autoincrement=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) package_id: str = sqlalchemy.Column(sqlalchemy.String, index=True) release_id: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) diff --git a/app/ch13-validation/starter/pypi_org/data/languages.py b/app/ch13-validation/starter/pypi_org/data/languages.py index 964b8444..3a73dde0 100644 --- a/app/ch13-validation/starter/pypi_org/data/languages.py +++ b/app/ch13-validation/starter/pypi_org/data/languages.py @@ -7,6 +7,5 @@ class ProgrammingLanguage(SqlAlchemyBase): __tablename__ = 'languages' id: str = sqlalchemy.Column(sqlalchemy.String, primary_key=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) description: str = sqlalchemy.Column(sqlalchemy.String) diff --git a/app/ch13-validation/starter/pypi_org/data/package.py b/app/ch13-validation/starter/pypi_org/data/package.py index c3420d1b..e0e48878 100644 --- a/app/ch13-validation/starter/pypi_org/data/package.py +++ b/app/ch13-validation/starter/pypi_org/data/package.py @@ -3,6 +3,8 @@ import sqlalchemy as sa import sqlalchemy.orm as orm +from sqlalchemy.orm import Mapped + from pypi_org.data.modelbase import SqlAlchemyBase from pypi_org.data.releases import Release @@ -12,7 +14,6 @@ class Package(SqlAlchemyBase): id: str = sa.Column(sa.String, primary_key=True) created_date: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) - last_updated: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) summary: str = sa.Column(sa.String, nullable=False) description: str = sa.Column(sa.String, nullable=True) @@ -26,11 +27,15 @@ class Package(SqlAlchemyBase): license: str = sa.Column(sa.String, index=True) # releases relationship - releases: List[Release] = orm.relation("Release", order_by=[ - Release.major_ver.desc(), - Release.minor_ver.desc(), - Release.build_ver.desc(), - ], back_populates='package') + releases: Mapped[Release] = orm.relationship( + 'Release', + order_by=[ + Release.major_ver.desc(), + Release.minor_ver.desc(), + Release.build_ver.desc(), + ], + back_populates='package', + ) def __repr__(self): return ''.format(self.id) diff --git a/app/ch13-validation/starter/pypi_org/data/releases.py b/app/ch13-validation/starter/pypi_org/data/releases.py index 7650e438..4b2db2bf 100644 --- a/app/ch13-validation/starter/pypi_org/data/releases.py +++ b/app/ch13-validation/starter/pypi_org/data/releases.py @@ -13,16 +13,14 @@ class Release(SqlAlchemyBase): minor_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) build_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) - created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, - default=datetime.datetime.now, - index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) comment: str = sqlalchemy.Column(sqlalchemy.String) url: str = sqlalchemy.Column(sqlalchemy.String) size: int = sqlalchemy.Column(sqlalchemy.BigInteger) # Package relationship - package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey("packages.id")) - package = orm.relation('Package') + package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey('packages.id')) + package = orm.relationship('Package') @property def version_text(self): diff --git a/app/ch13-validation/starter/pypi_org/infrastructure/cookie_auth.py b/app/ch13-validation/starter/pypi_org/infrastructure/cookie_auth.py index 64dc85a7..c215c0af 100644 --- a/app/ch13-validation/starter/pypi_org/infrastructure/cookie_auth.py +++ b/app/ch13-validation/starter/pypi_org/infrastructure/cookie_auth.py @@ -1,19 +1,18 @@ import hashlib -from datetime import timedelta from typing import Optional from flask import Request from flask import Response -from pypi_org.bin.load_data import try_int +from pypi_org.infrastructure.num_convert import try_int auth_cookie_name = 'pypi_demo_user' def set_auth(response: Response, user_id: int): hash_val = __hash_text(str(user_id)) - val = "{}:{}".format(user_id, hash_val) - response.set_cookie(auth_cookie_name, val) + val = '{}:{}'.format(user_id, hash_val) + response.set_cookie(auth_cookie_name, val, secure=False, httponly=True, samesite='Lax') def __hash_text(text: str) -> str: @@ -21,10 +20,6 @@ def __hash_text(text: str) -> str: return hashlib.sha512(text.encode('utf-8')).hexdigest() -def __add_cookie_callback(_, response: Response, name: str, value: str): - response.set_cookie(name, value, max_age=timedelta(days=30)) - - def get_user_id_via_auth_cookie(request: Request) -> Optional[int]: if auth_cookie_name not in request.cookies: return None @@ -38,7 +33,7 @@ def get_user_id_via_auth_cookie(request: Request) -> Optional[int]: hash_val = parts[1] hash_val_check = __hash_text(user_id) if hash_val != hash_val_check: - print("Warning: Hash mismatch, invalid cookie value") + print('Warning: Hash mismatch, invalid cookie value') return None return try_int(user_id) diff --git a/app/ch13-validation/starter/pypi_org/infrastructure/num_convert.py b/app/ch13-validation/starter/pypi_org/infrastructure/num_convert.py new file mode 100644 index 00000000..4e1d8879 --- /dev/null +++ b/app/ch13-validation/starter/pypi_org/infrastructure/num_convert.py @@ -0,0 +1,8 @@ +from typing import Optional + + +def try_int(text) -> Optional[int]: + try: + return int(text) + except: + return None diff --git a/app/ch13-validation/starter/pypi_org/infrastructure/request_dict.py b/app/ch13-validation/starter/pypi_org/infrastructure/request_dict.py index 495fd979..d44ae574 100644 --- a/app/ch13-validation/starter/pypi_org/infrastructure/request_dict.py +++ b/app/ch13-validation/starter/pypi_org/infrastructure/request_dict.py @@ -1,4 +1,5 @@ import flask +from werkzeug.datastructures import MultiDict class RequestDictionary(dict): @@ -13,11 +14,22 @@ def __getattr__(self, key): def create(default_val=None, **route_args) -> RequestDictionary: request = flask.request + # Adding this retro actively. Some folks are experiencing issues where they + # are getting a list rather than plain dict. I think it's from multiple + # entries in the multidict. This should fix it. + args = request.args + if isinstance(request.args, MultiDict): + args = request.args.to_dict() + + form = request.form + if isinstance(request.args, MultiDict): + form = request.form.to_dict() + data = { - **request.args, # The key/value pairs in the URL query string + **args, # The key/value pairs in the URL query string **request.headers, # Header values - **request.form, # The key/value pairs in the body, from a HTML post form - **route_args # And additional arguments the method access, if they want them merged. + **form, # The key/value pairs in the body, from a HTML post form + **route_args, # And additional arguments the method access, if they want them merged. } return RequestDictionary(data, default_val=default_val) diff --git a/app/ch13-validation/starter/pypi_org/infrastructure/view_modifiers.py b/app/ch13-validation/starter/pypi_org/infrastructure/view_modifiers.py index 82ce5baf..d944c142 100644 --- a/app/ch13-validation/starter/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch13-validation/starter/pypi_org/infrastructure/view_modifiers.py @@ -1,7 +1,8 @@ from functools import wraps import flask -import werkzeug.wrappers.response +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -11,9 +12,11 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) - if isinstance(response_val, flask.Response): + + if isinstance(response_val, werkzeug.wrappers.Response): return response_val - if isinstance(response_val, werkzeug.wrappers.response.Response): + + if isinstance(response_val, flask.Response): return response_val if isinstance(response_val, dict): @@ -23,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -39,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch13-validation/starter/pypi_org/services/package_service.py b/app/ch13-validation/starter/pypi_org/services/package_service.py index e51241ae..6ac445e6 100644 --- a/app/ch13-validation/starter/pypi_org/services/package_service.py +++ b/app/ch13-validation/starter/pypi_org/services/package_service.py @@ -9,11 +9,13 @@ def get_latest_releases(limit=10) -> List[Release]: session = db_session.create_session() - releases = session.query(Release). \ - options(sqlalchemy.orm.joinedload(Release.package)). \ - order_by(Release.created_date.desc()). \ - limit(limit). \ - all() + releases = ( + session.query(Release) + .options(sqlalchemy.orm.joinedload(Release.package)) + .order_by(Release.created_date.desc()) + .limit(limit) + .all() + ) session.close() @@ -38,10 +40,12 @@ def get_package_by_id(package_id: str) -> Optional[Package]: session = db_session.create_session() - package = session.query(Package) \ - .options(sqlalchemy.orm.joinedload(Package.releases)) \ - .filter(Package.id == package_id) \ + package = ( + session.query(Package) + .options(sqlalchemy.orm.joinedload(Package.releases)) + .filter(Package.id == package_id) .first() + ) session.close() diff --git a/app/ch13-validation/starter/pypi_org/views/account_views.py b/app/ch13-validation/starter/pypi_org/views/account_views.py index f127310b..877231ba 100644 --- a/app/ch13-validation/starter/pypi_org/views/account_views.py +++ b/app/ch13-validation/starter/pypi_org/views/account_views.py @@ -30,6 +30,7 @@ def index(): # ################### REGISTER ################################# + @blueprint.route('/account/register', methods=['GET']) @response(template_file='account/register.html') def register_get(): @@ -52,7 +53,7 @@ def register_post(): 'name': name, 'email': email, 'password': password, - 'error': "Some required fields are missing.", + 'error': 'Some required fields are missing.', 'user_id': cookie_auth.get_user_id_via_auth_cookie(flask.request), } @@ -62,7 +63,7 @@ def register_post(): 'name': name, 'email': email, 'password': password, - 'error': "A user with that email already exists.", + 'error': 'A user with that email already exists.', 'user_id': cookie_auth.get_user_id_via_auth_cookie(flask.request), } @@ -74,6 +75,7 @@ def register_post(): # ################### LOGIN ################################# + @blueprint.route('/account/login', methods=['GET']) @response(template_file='account/login.html') def login_get(): @@ -92,7 +94,7 @@ def login_post(): return { 'email': email, 'password': password, - 'error': "Some required fields are missing.", + 'error': 'Some required fields are missing.', 'user_id': cookie_auth.get_user_id_via_auth_cookie(flask.request), } @@ -101,7 +103,7 @@ def login_post(): return { 'email': email, 'password': password, - 'error': "The account does not exist or the password is wrong.", + 'error': 'The account does not exist or the password is wrong.', 'user_id': cookie_auth.get_user_id_via_auth_cookie(flask.request), } @@ -113,6 +115,7 @@ def login_post(): # ################### LOGOUT ################################# + @blueprint.route('/account/logout') def logout(): resp = flask.redirect('/') diff --git a/app/ch13-validation/starter/pypi_org/views/cms_views.py b/app/ch13-validation/starter/pypi_org/views/cms_views.py index f7172d3d..a8b755a8 100644 --- a/app/ch13-validation/starter/pypi_org/views/cms_views.py +++ b/app/ch13-validation/starter/pypi_org/views/cms_views.py @@ -9,7 +9,7 @@ @blueprint.route('/') @response(template_file='cms/page.html') def cms_page(full_url: str): - print("Getting CMS page for {}".format(full_url)) + print('Getting CMS page for {}'.format(full_url)) page = cms_service.get_page(full_url) if not page: diff --git a/app/ch13-validation/starter/pypi_org/views/package_views.py b/app/ch13-validation/starter/pypi_org/views/package_views.py index f1ad7811..e634d845 100644 --- a/app/ch13-validation/starter/pypi_org/views/package_views.py +++ b/app/ch13-validation/starter/pypi_org/views/package_views.py @@ -17,12 +17,12 @@ def package_details(package_name: str): if not package: return flask.abort(status=404) - latest_version = "0.0.0" + latest_version = '0.0.0' latest_release = None is_latest = True if package.releases: - latest_release = package.releases[0] + latest_release = package.releases latest_version = latest_release.version_text return { @@ -38,4 +38,4 @@ def package_details(package_name: str): @blueprint.route('/') def popular(rank: int): print(type(rank), rank) - return "The details for the {}th most popular package".format(rank) + return 'The details for the {}th most popular package'.format(rank) diff --git a/app/ch13-validation/starter/requirements.piptools b/app/ch13-validation/starter/requirements.piptools new file mode 100644 index 00000000..d13bf0c8 --- /dev/null +++ b/app/ch13-validation/starter/requirements.piptools @@ -0,0 +1,6 @@ +alembic +flask +passlib +progressbar2 +python-dateutil +sqlalchemy diff --git a/app/ch13-validation/starter/requirements.txt b/app/ch13-validation/starter/requirements.txt index e10973d8..19fc8c45 100644 --- a/app/ch13-validation/starter/requirements.txt +++ b/app/ch13-validation/starter/requirements.txt @@ -1,8 +1,42 @@ -werkzeug -flask -sqlalchemy - -progressbar2 -python-dateutil -passlib - +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +alembic==1.14.1 + # via -r requirements.piptools +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +mako==1.3.9 + # via alembic +markupsafe==3.0.2 + # via + # jinja2 + # mako + # werkzeug +passlib==1.7.4 + # via -r requirements.piptools +progressbar2==4.5.0 + # via -r requirements.piptools +python-dateutil==2.9.0.post0 + # via -r requirements.piptools +python-utils==3.9.1 + # via progressbar2 +six==1.17.0 + # via python-dateutil +sqlalchemy==2.0.38 + # via + # -r requirements.piptools + # alembic +typing-extensions==4.12.2 + # via + # alembic + # python-utils + # sqlalchemy +werkzeug==3.1.3 + # via flask diff --git a/app/ch14_testing/final/alembic/alembic_helpers.py b/app/ch14_testing/final/alembic/alembic_helpers.py index 561f7a70..6aea52d4 100644 --- a/app/ch14_testing/final/alembic/alembic_helpers.py +++ b/app/ch14_testing/final/alembic/alembic_helpers.py @@ -5,8 +5,7 @@ def table_has_column(table, column): config = op.get_context().config - engine = engine_from_config( - config.get_section(config.config_ini_section), prefix='sqlalchemy.') + engine = engine_from_config(config.get_section(config.config_ini_section), prefix='sqlalchemy.') insp = reflection.Inspector.from_engine(engine) has_column = False for col in insp.get_columns(table): diff --git a/app/ch14_testing/final/alembic/env.py b/app/ch14_testing/final/alembic/env.py index 27ec49fb..1fdc9cab 100644 --- a/app/ch14_testing/final/alembic/env.py +++ b/app/ch14_testing/final/alembic/env.py @@ -26,6 +26,7 @@ sys.path.insert(0, folder) from pypi_org.data.modelbase import SqlAlchemyBase + # noinspection PyUnresolvedReferences import pypi_org.data.__all_models @@ -49,10 +50,8 @@ def run_migrations_offline(): script output. """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, target_metadata=target_metadata, literal_binds=True - ) + url = config.get_main_option('sqlalchemy.url') + context.configure(url=url, target_metadata=target_metadata, literal_binds=True) with context.begin_transaction(): context.run_migrations() @@ -67,14 +66,12 @@ def run_migrations_online(): """ connectable = engine_from_config( config.get_section(config.config_ini_section), - prefix="sqlalchemy.", + prefix='sqlalchemy.', poolclass=pool.NullPool, ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() diff --git a/app/ch14_testing/final/alembic/versions/722c82f0097c_added_auditing_table.py b/app/ch14_testing/final/alembic/versions/722c82f0097c_added_auditing_table.py index 2c3e2a39..7c6e892f 100644 --- a/app/ch14_testing/final/alembic/versions/722c82f0097c_added_auditing_table.py +++ b/app/ch14_testing/final/alembic/versions/722c82f0097c_added_auditing_table.py @@ -18,11 +18,12 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('auditing', - sa.Column('id', sa.String(), nullable=False), - sa.Column('created_date', sa.DateTime(), nullable=True), - sa.Column('description', sa.String(), nullable=True), - sa.PrimaryKeyConstraint('id') + op.create_table( + 'auditing', + sa.Column('id', sa.String(), nullable=False), + sa.Column('created_date', sa.DateTime(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id'), ) op.create_index(op.f('ix_auditing_created_date'), 'auditing', ['created_date'], unique=False) # ### end Alembic commands ### diff --git a/app/ch14_testing/final/pypi_org/app.py b/app/ch14_testing/final/pypi_org/app.py index 6ecc1c13..23eafbe9 100644 --- a/app/ch14_testing/final/pypi_org/app.py +++ b/app/ch14_testing/final/pypi_org/app.py @@ -2,6 +2,7 @@ import sys import flask + folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, folder) import pypi_org.data.db_session as db_session @@ -16,10 +17,7 @@ def main(): def setup_db(): - db_file = os.path.join( - os.path.dirname(__file__), - 'db', - 'pypi.sqlite') + db_file = os.path.join(os.path.dirname(__file__), 'db', 'pypi.sqlite') db_session.global_init(db_file) diff --git a/app/ch14_testing/final/pypi_org/bin/basic_inserts.py b/app/ch14_testing/final/pypi_org/bin/basic_inserts.py index 3a2f360d..6d04f551 100644 --- a/app/ch14_testing/final/pypi_org/bin/basic_inserts.py +++ b/app/ch14_testing/final/pypi_org/bin/basic_inserts.py @@ -1,14 +1,13 @@ import os import sys +# Make it run more easily outside of PyCharm +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + import pypi_org.data.db_session as db_session from pypi_org.data.package import Package from pypi_org.data.releases import Release -# Make it run more easily outside of PyCharm -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) - def main(): init_db() @@ -20,24 +19,24 @@ def insert_a_package(): p = Package() p.id = input('Package id / name: ').strip().lower() - p.summary = input("Package summary: ").strip() - p.author_name = input("Author: ").strip() - p.license = input("License: ").strip() + p.summary = input('Package summary: ').strip() + p.author_name = input('Author: ').strip() + p.license = input('License: ').strip() - print("Release 1:") + print('Release 1:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) - print("Release 2:") + print('Release 2:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) session = db_session.create_session() diff --git a/app/ch14_testing/final/pypi_org/bin/load_data.py b/app/ch14_testing/final/pypi_org/bin/load_data.py index 15237f18..3af7a03d 100644 --- a/app/ch14_testing/final/pypi_org/bin/load_data.py +++ b/app/ch14_testing/final/pypi_org/bin/load_data.py @@ -7,9 +7,9 @@ import progressbar from dateutil.parser import parse -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) +from pypi_org.infrastructure.num_convert import try_int import pypi_org.data.db_session as db_session from pypi_org.data.languages import ProgrammingLanguage from pypi_org.data.licenses import License @@ -39,7 +39,7 @@ def main(): def do_import_languages(file_data: List[dict]): imported = set() - print("Importing languages ... ", flush=True) + print('Importing languages ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -74,7 +74,7 @@ def do_import_languages(file_data: List[dict]): def do_import_licenses(file_data: List[dict]): imported = set() - print("Importing licenses ... ", flush=True) + print('Importing licenses ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -100,17 +100,17 @@ def do_import_licenses(file_data: List[dict]): def do_summary(): session = db_session.create_session() - print("Final numbers:") - print("Users: {:,}".format(session.query(User).count())) - print("Packages: {:,}".format(session.query(Package).count())) - print("Releases: {:,}".format(session.query(Release).count())) - print("Maintainers: {:,}".format(session.query(Maintainer).count())) - print("Languages: {:,}".format(session.query(ProgrammingLanguage).count())) - print("Licenses: {:,}".format(session.query(License).count())) + print('Final numbers:') + print('Users: {:,}'.format(session.query(User).count())) + print('Packages: {:,}'.format(session.query(Package).count())) + print('Releases: {:,}'.format(session.query(Release).count())) + print('Maintainers: {:,}'.format(session.query(Maintainer).count())) + print('Languages: {:,}'.format(session.query(ProgrammingLanguage).count())) + print('Licenses: {:,}'.format(session.query(License).count())) def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: - print("Importing users ... ", flush=True) + print('Importing users ... ', flush=True) with progressbar.ProgressBar(max_value=len(user_lookup)) as bar: for idx, (email, name) in enumerate(user_lookup.items()): session = db_session.create_session() @@ -134,29 +134,29 @@ def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: def do_import_packages(file_data: List[dict], user_lookup: Dict[str, User]): errored_packages = [] - print("Importing packages and releases ... ", flush=True) + print('Importing packages and releases ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): try: load_package(p, user_lookup) bar.update(idx) except Exception as x: - errored_packages.append((p, " *** Errored out for package {}, {}".format(p.get('package_name'), x))) + errored_packages.append((p, ' *** Errored out for package {}, {}'.format(p.get('package_name'), x))) raise sys.stderr.flush() sys.stdout.flush() print() - print("Completed packages with {} errors.".format(len(errored_packages))) - for (p, txt) in errored_packages: + print('Completed packages with {} errors.'.format(len(errored_packages))) + for p, txt in errored_packages: print(txt) def do_load_files() -> List[dict]: data_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../../data/pypi-top-100')) - print("Loading files from {}".format(data_path)) + print('Loading files from {}'.format(data_path)) files = get_file_names(data_path) - print("Found {:,} files, loading ...".format(len(files)), flush=True) - time.sleep(.1) + print('Found {:,} files, loading ...'.format(len(files)), flush=True) + time.sleep(0.1) file_data = [] with progressbar.ProgressBar(max_value=len(files)) as bar: @@ -171,7 +171,7 @@ def do_load_files() -> List[dict]: def find_users(data: List[dict]) -> dict: - print("Discovering users...", flush=True) + print('Discovering users...', flush=True) found_users = {} with progressbar.ProgressBar(max_value=len(data)) as bar: @@ -184,7 +184,7 @@ def find_users(data: List[dict]) -> dict: sys.stderr.flush() sys.stdout.flush() print() - print("Discovered {:,} users".format(len(found_users))) + print('Discovered {:,} users'.format(len(found_users))) print() return found_users @@ -215,7 +215,7 @@ def load_file_data(filename: str) -> dict: with open(filename, 'r', encoding='utf-8') as fin: data = json.load(fin) except Exception as x: - print("ERROR in file: {}, details: {}".format(filename, x), flush=True) + print('ERROR in file: {}, details: {}'.format(filename, x), flush=True) raise return data @@ -233,7 +233,7 @@ def load_package(data: dict, user_lookup: Dict[str, User]): p.author = info.get('author') p.author_email = info.get('author_email') - releases = build_releases(p.id, data.get("releases", {})) + releases = build_releases(p.id, data.get('releases', {})) if releases: p.created_date = releases[0].created_date @@ -283,18 +283,13 @@ def detect_license(license_text: str) -> Optional[str]: license_text = license_text.strip() if len(license_text) > 100 or '\n' in license_text: - return "CUSTOM" + return 'CUSTOM' - license_text = license_text \ - .replace('Software License', '') \ - .replace('License', '') + license_text = license_text.replace('Software License', '').replace('License', '') if '::' in license_text: # E.g. 'License :: OSI Approved :: Apache Software License' - return license_text \ - .split(':')[-1] \ - .replace(' ', ' ') \ - .strip() + return license_text.split(':')[-1].replace(' ', ' ').strip() return license_text.strip() @@ -339,13 +334,6 @@ def make_version_num(version_text): return major, minor, build -def try_int(text) -> int: - try: - return int(text) - except: - return 0 - - def init_db(): top_folder = os.path.dirname(__file__) rel_file = os.path.join('..', 'db', 'pypi.sqlite') @@ -357,9 +345,7 @@ def get_file_names(data_path: str) -> List[str]: files = [] for f in os.listdir(data_path): if f.endswith('.json'): - files.append( - os.path.abspath(os.path.join(data_path, f)) - ) + files.append(os.path.abspath(os.path.join(data_path, f))) files.sort() return files diff --git a/app/ch14_testing/final/pypi_org/data/__all_models.py b/app/ch14_testing/final/pypi_org/data/__all_models.py index f399ef54..ca4804ac 100644 --- a/app/ch14_testing/final/pypi_org/data/__all_models.py +++ b/app/ch14_testing/final/pypi_org/data/__all_models.py @@ -5,17 +5,24 @@ # noinspection PyUnresolvedReferences import pypi_org.data.audit + # noinspection PyUnresolvedReferences import pypi_org.data.downloads + # noinspection PyUnresolvedReferences import pypi_org.data.languages + # noinspection PyUnresolvedReferences import pypi_org.data.licenses + # noinspection PyUnresolvedReferences import pypi_org.data.maintainers + # noinspection PyUnresolvedReferences import pypi_org.data.package + # noinspection PyUnresolvedReferences import pypi_org.data.releases + # noinspection PyUnresolvedReferences import pypi_org.data.users diff --git a/app/ch14_testing/final/pypi_org/data/db_session.py b/app/ch14_testing/final/pypi_org/data/db_session.py index acfc485d..86eb3fb2 100644 --- a/app/ch14_testing/final/pypi_org/data/db_session.py +++ b/app/ch14_testing/final/pypi_org/data/db_session.py @@ -14,12 +14,15 @@ def global_init(db_file: str): return if not db_file or not db_file.strip(): - raise Exception("You must specify a db file.") + raise Exception('You must specify a db file.') conn_str = 'sqlite:///' + db_file.strip() - print("Connecting to DB with {}".format(conn_str)) + print('Connecting to DB with {}'.format(conn_str)) - engine = sa.create_engine(conn_str, echo=False) + # Adding check_same_thread = False after the recording. This can be an issue about + # creating / owner thread when cleaning up sessions, etc. This is a sqlite restriction + # that we probably don't care about in this example. + engine = sa.create_engine(conn_str, echo=False, connect_args={'check_same_thread': False}) __factory = orm.sessionmaker(bind=engine) # noinspection PyUnresolvedReferences diff --git a/app/ch14_testing/final/pypi_org/data/downloads.py b/app/ch14_testing/final/pypi_org/data/downloads.py index 66068f02..dd8afd65 100644 --- a/app/ch14_testing/final/pypi_org/data/downloads.py +++ b/app/ch14_testing/final/pypi_org/data/downloads.py @@ -7,8 +7,7 @@ class Download(SqlAlchemyBase): __tablename__ = 'downloads' id: int = sqlalchemy.Column(sqlalchemy.BigInteger, primary_key=True, autoincrement=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) package_id: str = sqlalchemy.Column(sqlalchemy.String, index=True) release_id: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) diff --git a/app/ch14_testing/final/pypi_org/data/languages.py b/app/ch14_testing/final/pypi_org/data/languages.py index 964b8444..3a73dde0 100644 --- a/app/ch14_testing/final/pypi_org/data/languages.py +++ b/app/ch14_testing/final/pypi_org/data/languages.py @@ -7,6 +7,5 @@ class ProgrammingLanguage(SqlAlchemyBase): __tablename__ = 'languages' id: str = sqlalchemy.Column(sqlalchemy.String, primary_key=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) description: str = sqlalchemy.Column(sqlalchemy.String) diff --git a/app/ch14_testing/final/pypi_org/data/package.py b/app/ch14_testing/final/pypi_org/data/package.py index c3420d1b..e0e48878 100644 --- a/app/ch14_testing/final/pypi_org/data/package.py +++ b/app/ch14_testing/final/pypi_org/data/package.py @@ -3,6 +3,8 @@ import sqlalchemy as sa import sqlalchemy.orm as orm +from sqlalchemy.orm import Mapped + from pypi_org.data.modelbase import SqlAlchemyBase from pypi_org.data.releases import Release @@ -12,7 +14,6 @@ class Package(SqlAlchemyBase): id: str = sa.Column(sa.String, primary_key=True) created_date: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) - last_updated: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) summary: str = sa.Column(sa.String, nullable=False) description: str = sa.Column(sa.String, nullable=True) @@ -26,11 +27,15 @@ class Package(SqlAlchemyBase): license: str = sa.Column(sa.String, index=True) # releases relationship - releases: List[Release] = orm.relation("Release", order_by=[ - Release.major_ver.desc(), - Release.minor_ver.desc(), - Release.build_ver.desc(), - ], back_populates='package') + releases: Mapped[Release] = orm.relationship( + 'Release', + order_by=[ + Release.major_ver.desc(), + Release.minor_ver.desc(), + Release.build_ver.desc(), + ], + back_populates='package', + ) def __repr__(self): return ''.format(self.id) diff --git a/app/ch14_testing/final/pypi_org/data/releases.py b/app/ch14_testing/final/pypi_org/data/releases.py index 7650e438..4b2db2bf 100644 --- a/app/ch14_testing/final/pypi_org/data/releases.py +++ b/app/ch14_testing/final/pypi_org/data/releases.py @@ -13,16 +13,14 @@ class Release(SqlAlchemyBase): minor_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) build_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) - created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, - default=datetime.datetime.now, - index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) comment: str = sqlalchemy.Column(sqlalchemy.String) url: str = sqlalchemy.Column(sqlalchemy.String) size: int = sqlalchemy.Column(sqlalchemy.BigInteger) # Package relationship - package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey("packages.id")) - package = orm.relation('Package') + package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey('packages.id')) + package = orm.relationship('Package') @property def version_text(self): diff --git a/app/ch14_testing/final/pypi_org/infrastructure/cookie_auth.py b/app/ch14_testing/final/pypi_org/infrastructure/cookie_auth.py index 64dc85a7..d8f2b4ec 100644 --- a/app/ch14_testing/final/pypi_org/infrastructure/cookie_auth.py +++ b/app/ch14_testing/final/pypi_org/infrastructure/cookie_auth.py @@ -1,19 +1,18 @@ import hashlib -from datetime import timedelta from typing import Optional from flask import Request from flask import Response -from pypi_org.bin.load_data import try_int +from pypi_org.infrastructure.num_convert import try_int auth_cookie_name = 'pypi_demo_user' def set_auth(response: Response, user_id: int): hash_val = __hash_text(str(user_id)) - val = "{}:{}".format(user_id, hash_val) - response.set_cookie(auth_cookie_name, val) + val = '{}:{}'.format(user_id, hash_val) + response.set_cookie(auth_cookie_name, val, secure=True, httponly=True, samesite='Lax') def __hash_text(text: str) -> str: @@ -21,10 +20,6 @@ def __hash_text(text: str) -> str: return hashlib.sha512(text.encode('utf-8')).hexdigest() -def __add_cookie_callback(_, response: Response, name: str, value: str): - response.set_cookie(name, value, max_age=timedelta(days=30)) - - def get_user_id_via_auth_cookie(request: Request) -> Optional[int]: if auth_cookie_name not in request.cookies: return None @@ -38,7 +33,7 @@ def get_user_id_via_auth_cookie(request: Request) -> Optional[int]: hash_val = parts[1] hash_val_check = __hash_text(user_id) if hash_val != hash_val_check: - print("Warning: Hash mismatch, invalid cookie value") + print('Warning: Hash mismatch, invalid cookie value') return None return try_int(user_id) diff --git a/app/ch14_testing/final/pypi_org/infrastructure/num_convert.py b/app/ch14_testing/final/pypi_org/infrastructure/num_convert.py new file mode 100644 index 00000000..4e1d8879 --- /dev/null +++ b/app/ch14_testing/final/pypi_org/infrastructure/num_convert.py @@ -0,0 +1,8 @@ +from typing import Optional + + +def try_int(text) -> Optional[int]: + try: + return int(text) + except: + return None diff --git a/app/ch14_testing/final/pypi_org/infrastructure/request_dict.py b/app/ch14_testing/final/pypi_org/infrastructure/request_dict.py index 495fd979..d44ae574 100644 --- a/app/ch14_testing/final/pypi_org/infrastructure/request_dict.py +++ b/app/ch14_testing/final/pypi_org/infrastructure/request_dict.py @@ -1,4 +1,5 @@ import flask +from werkzeug.datastructures import MultiDict class RequestDictionary(dict): @@ -13,11 +14,22 @@ def __getattr__(self, key): def create(default_val=None, **route_args) -> RequestDictionary: request = flask.request + # Adding this retro actively. Some folks are experiencing issues where they + # are getting a list rather than plain dict. I think it's from multiple + # entries in the multidict. This should fix it. + args = request.args + if isinstance(request.args, MultiDict): + args = request.args.to_dict() + + form = request.form + if isinstance(request.args, MultiDict): + form = request.form.to_dict() + data = { - **request.args, # The key/value pairs in the URL query string + **args, # The key/value pairs in the URL query string **request.headers, # Header values - **request.form, # The key/value pairs in the body, from a HTML post form - **route_args # And additional arguments the method access, if they want them merged. + **form, # The key/value pairs in the body, from a HTML post form + **route_args, # And additional arguments the method access, if they want them merged. } return RequestDictionary(data, default_val=default_val) diff --git a/app/ch14_testing/final/pypi_org/infrastructure/view_modifiers.py b/app/ch14_testing/final/pypi_org/infrastructure/view_modifiers.py index 82ce5baf..d944c142 100644 --- a/app/ch14_testing/final/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch14_testing/final/pypi_org/infrastructure/view_modifiers.py @@ -1,7 +1,8 @@ from functools import wraps import flask -import werkzeug.wrappers.response +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -11,9 +12,11 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) - if isinstance(response_val, flask.Response): + + if isinstance(response_val, werkzeug.wrappers.Response): return response_val - if isinstance(response_val, werkzeug.wrappers.response.Response): + + if isinstance(response_val, flask.Response): return response_val if isinstance(response_val, dict): @@ -23,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -39,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch14_testing/final/pypi_org/services/package_service.py b/app/ch14_testing/final/pypi_org/services/package_service.py index b03408d2..9a153773 100644 --- a/app/ch14_testing/final/pypi_org/services/package_service.py +++ b/app/ch14_testing/final/pypi_org/services/package_service.py @@ -10,12 +10,13 @@ def get_latest_releases(limit=10) -> List[Release]: session = db_session.create_session() try: - - releases = session.query(Release). \ - options(sqlalchemy.orm.joinedload(Release.package)). \ - order_by(Release.created_date.desc()). \ - limit(limit). \ - all() + releases = ( + session.query(Release) + .options(sqlalchemy.orm.joinedload(Release.package)) + .order_by(Release.created_date.desc()) + .limit(limit) + .all() + ) finally: session.close() @@ -47,11 +48,12 @@ def get_package_by_id(package_id: str) -> Optional[Package]: session = db_session.create_session() try: - - package = session.query(Package) \ - .options(sqlalchemy.orm.joinedload(Package.releases)) \ - .filter(Package.id == package_id) \ + package = ( + session.query(Package) + .options(sqlalchemy.orm.joinedload(Package.releases)) + .filter(Package.id == package_id) .first() + ) finally: session.close() diff --git a/app/ch14_testing/final/pypi_org/viewmodels/cms/page_viewmodel.py b/app/ch14_testing/final/pypi_org/viewmodels/cms/page_viewmodel.py index b7bb3249..ec033c3f 100644 --- a/app/ch14_testing/final/pypi_org/viewmodels/cms/page_viewmodel.py +++ b/app/ch14_testing/final/pypi_org/viewmodels/cms/page_viewmodel.py @@ -7,4 +7,3 @@ def __init__(self, full_url: str): super().__init__() self.page = cms_service.get_page(full_url) - diff --git a/app/ch14_testing/final/pypi_org/viewmodels/packages/pagedetails_viewmodel.py b/app/ch14_testing/final/pypi_org/viewmodels/packages/pagedetails_viewmodel.py index 63c4cf68..62bdca3d 100644 --- a/app/ch14_testing/final/pypi_org/viewmodels/packages/pagedetails_viewmodel.py +++ b/app/ch14_testing/final/pypi_org/viewmodels/packages/pagedetails_viewmodel.py @@ -11,12 +11,12 @@ def __init__(self, package_name: str): self.package_name = package_name.strip().lower() self.package = package_service.get_package_by_id(self.package_name) - self.latest_version = "0.0.0" + self.latest_version = '0.0.0' self.latest_release = None self.is_latest = True if self.package and self.package.releases: - self.latest_release = self.package.releases[0] + self.latest_release = self.package.releases self.latest_version = self.latest_release.version_text self.release_version = self.latest_release diff --git a/app/ch14_testing/final/pypi_org/viewmodels/seo/sitemap_viewmodel.py b/app/ch14_testing/final/pypi_org/viewmodels/seo/sitemap_viewmodel.py index 912c4df0..71df2f99 100644 --- a/app/ch14_testing/final/pypi_org/viewmodels/seo/sitemap_viewmodel.py +++ b/app/ch14_testing/final/pypi_org/viewmodels/seo/sitemap_viewmodel.py @@ -8,5 +8,5 @@ class SiteMapViewModel(ViewModelBase): def __init__(self, limit: int): super().__init__() self.packages = package_service.all_packages(limit) - self.last_updated_text = "2019-07-15" - self.site = "{}://{}".format(flask.request.scheme, flask.request.host) + self.last_updated_text = '2019-07-15' + self.site = '{}://{}'.format(flask.request.scheme, flask.request.host) diff --git a/app/ch14_testing/final/pypi_org/views/account_views.py b/app/ch14_testing/final/pypi_org/views/account_views.py index 8697cc24..652f43b3 100644 --- a/app/ch14_testing/final/pypi_org/views/account_views.py +++ b/app/ch14_testing/final/pypi_org/views/account_views.py @@ -25,6 +25,7 @@ def index(): # ################### REGISTER ################################# + @blueprint.route('/account/register', methods=['GET']) @response(template_file='account/register.html') def register_get(): @@ -54,10 +55,16 @@ def register_post(): # ################### LOGIN ################################# + @blueprint.route('/account/login', methods=['GET']) @response(template_file='account/login.html') def login_get(): vm = LoginViewModel() + + # Added after recording, see https://github.com/talkpython/data-driven-web-apps-with-flask/issues/24 + if vm.user_id: + return flask.redirect('/account') + return vm.to_dict() @@ -72,7 +79,7 @@ def login_post(): user = user_service.login_user(vm.email, vm.password) if not user: - vm.error = "The account does not exist or the password is wrong." + vm.error = 'The account does not exist or the password is wrong.' return vm.to_dict() resp = flask.redirect('/account') @@ -83,6 +90,7 @@ def login_post(): # ################### LOGOUT ################################# + @blueprint.route('/account/logout') def logout(): resp = flask.redirect('/') diff --git a/app/ch14_testing/final/pypi_org/views/package_views.py b/app/ch14_testing/final/pypi_org/views/package_views.py index 1a74549f..2f862740 100644 --- a/app/ch14_testing/final/pypi_org/views/package_views.py +++ b/app/ch14_testing/final/pypi_org/views/package_views.py @@ -19,4 +19,4 @@ def package_details(package_name: str): @blueprint.route('/') def popular(rank: int): print(type(rank), rank) - return "The details for the {}th most popular package".format(rank) + return 'The details for the {}th most popular package'.format(rank) diff --git a/app/ch14_testing/final/pypi_org/views/seo_view.py b/app/ch14_testing/final/pypi_org/views/seo_view.py index 93c88de5..f86b6bff 100644 --- a/app/ch14_testing/final/pypi_org/views/seo_view.py +++ b/app/ch14_testing/final/pypi_org/views/seo_view.py @@ -18,6 +18,7 @@ def sitemap(): # ################### Robots ################################# + @blueprint.route('/robots.txt') @response(mimetype='text/plain', template_file='seo/robots.txt') def robots(): diff --git a/app/ch14_testing/final/requirements.piptools b/app/ch14_testing/final/requirements.piptools new file mode 100644 index 00000000..d13bf0c8 --- /dev/null +++ b/app/ch14_testing/final/requirements.piptools @@ -0,0 +1,6 @@ +alembic +flask +passlib +progressbar2 +python-dateutil +sqlalchemy diff --git a/app/ch14_testing/final/requirements.txt b/app/ch14_testing/final/requirements.txt index e10973d8..19fc8c45 100644 --- a/app/ch14_testing/final/requirements.txt +++ b/app/ch14_testing/final/requirements.txt @@ -1,8 +1,42 @@ -werkzeug -flask -sqlalchemy - -progressbar2 -python-dateutil -passlib - +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +alembic==1.14.1 + # via -r requirements.piptools +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +mako==1.3.9 + # via alembic +markupsafe==3.0.2 + # via + # jinja2 + # mako + # werkzeug +passlib==1.7.4 + # via -r requirements.piptools +progressbar2==4.5.0 + # via -r requirements.piptools +python-dateutil==2.9.0.post0 + # via -r requirements.piptools +python-utils==3.9.1 + # via progressbar2 +six==1.17.0 + # via python-dateutil +sqlalchemy==2.0.38 + # via + # -r requirements.piptools + # alembic +typing-extensions==4.12.2 + # via + # alembic + # python-utils + # sqlalchemy +werkzeug==3.1.3 + # via flask diff --git a/app/ch14_testing/final/tests/_all_tests.py b/app/ch14_testing/final/tests/_all_tests.py index daa69ea4..e5c9b9ea 100644 --- a/app/ch14_testing/final/tests/_all_tests.py +++ b/app/ch14_testing/final/tests/_all_tests.py @@ -1,17 +1,18 @@ import sys import os -container_folder = os.path.abspath(os.path.join( - os.path.dirname(__file__), '..' -)) +container_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, container_folder) # noinspection PyUnresolvedReferences from account_tests import * + # noinspection PyUnresolvedReferences from package_tests import * + # noinspection PyUnresolvedReferences from sitemap_tests import * + # noinspection PyUnresolvedReferences from home_tests import * diff --git a/app/ch14_testing/final/tests/account_tests.py b/app/ch14_testing/final/tests/account_tests.py index 4f5aa156..6f8a65a7 100644 --- a/app/ch14_testing/final/tests/account_tests.py +++ b/app/ch14_testing/final/tests/account_tests.py @@ -2,12 +2,12 @@ from pypi_org.data.users import User from pypi_org.viewmodels.account.register_viewmodel import RegisterViewModel -from tests.test_client import flask_app, client +from tests.test_client import flask_app import unittest.mock def test_example(): - print("Test example...") + print('Test example...') assert 1 + 2 == 3 @@ -15,11 +15,7 @@ def test_vm_register_validation_when_valid(): # 3 A's of test: Arrange, Act, then Assert # Arrange - form_data = { - 'name': 'Michael', - 'email': 'michael@talkpython.fm', - 'password': 'a' * 6 - } + form_data = {'name': 'Michael', 'email': 'michael@talkpython.fm', 'password': 'a' * 6} with flask_app.test_request_context(path='/account/register', data=form_data): vm = RegisterViewModel() @@ -37,11 +33,7 @@ def test_vm_register_validation_for_existing_user(): # 3 A's of test: Arrange, Act, then Assert # Arrange - form_data = { - 'name': 'Michael', - 'email': 'michael@talkpython.fm', - 'password': 'a' * 6 - } + form_data = {'name': 'Michael', 'email': 'michael@talkpython.fm', 'password': 'a' * 6} with flask_app.test_request_context(path='/account/register', data=form_data): vm = RegisterViewModel() @@ -62,11 +54,8 @@ def test_v_register_view_new_user(): # Arrange from pypi_org.views.account_views import register_post - form_data = { - 'name': 'Michael', - 'email': 'michael@talkpython.fm', - 'password': 'a' * 6 - } + + form_data = {'name': 'Michael', 'email': 'michael@talkpython.fm', 'password': 'a' * 6} target = 'pypi_org.services.user_service.find_user_by_email' find_user = unittest.mock.patch(target, return_value=None) diff --git a/app/ch14_testing/final/tests/conftest.py b/app/ch14_testing/final/tests/conftest.py new file mode 100644 index 00000000..a682b75c --- /dev/null +++ b/app/ch14_testing/final/tests/conftest.py @@ -0,0 +1,6 @@ +from test_client import * + +# Let's be sure to use conftest.py for sharing fixtures across multiple files +# Added after the recording but will help some folks. +# For more info, see: +# https://docs.pytest.org/en/6.2.x/fixture.html#conftest-py-sharing-fixtures-across-multiple-files diff --git a/app/ch14_testing/final/tests/home_tests.py b/app/ch14_testing/final/tests/home_tests.py index b95f48fb..b0e030be 100644 --- a/app/ch14_testing/final/tests/home_tests.py +++ b/app/ch14_testing/final/tests/home_tests.py @@ -1,6 +1,6 @@ from flask import Response -from tests.test_client import client, flask_app +from tests.test_client import flask_app from pypi_org.views import home_views diff --git a/app/ch14_testing/final/tests/package_tests.py b/app/ch14_testing/final/tests/package_tests.py index efd04ef5..a7c221a5 100644 --- a/app/ch14_testing/final/tests/package_tests.py +++ b/app/ch14_testing/final/tests/package_tests.py @@ -12,15 +12,14 @@ def test_package_details_success(): test_package = Package() test_package.id = 'sqlalchemy' - test_package.description = "TDB" + test_package.description = 'TDB' test_package.releases = [ Release(created_date=datetime.datetime.now(), major_ver=1, minor_ver=2, build_ver=200), Release(created_date=datetime.datetime.now() - datetime.timedelta(days=10)), ] # Act - with unittest.mock.patch('pypi_org.services.package_service.get_package_by_id', - return_value=test_package): + with unittest.mock.patch('pypi_org.services.package_service.get_package_by_id', return_value=test_package): with flask_app.test_request_context(path='/project/' + test_package.id): resp: Response = package_details(test_package.id) @@ -33,8 +32,7 @@ def test_package_details_404(client): bad_package_url = 'sqlalchemy_missing' # Act - with unittest.mock.patch('pypi_org.services.package_service.get_package_by_id', - return_value=None): + with unittest.mock.patch('pypi_org.services.package_service.get_package_by_id', return_value=None): resp: Response = client.get(bad_package_url) assert resp.status_code == 404 diff --git a/app/ch14_testing/final/tests/sitemap_tests.py b/app/ch14_testing/final/tests/sitemap_tests.py index b1afe0fe..4494c30a 100644 --- a/app/ch14_testing/final/tests/sitemap_tests.py +++ b/app/ch14_testing/final/tests/sitemap_tests.py @@ -10,10 +10,7 @@ def test_int_site_mapped_urls(client): href.text.strip().replace('http://127.0.0.1:5000', '').replace('http://localhost', '') for href in list(x.findall('url/loc')) ] - urls = [ - u if u else '/' - for u in urls - ] + urls = [u if u else '/' for u in urls] print('Testing {} urls from sitemap...'.format(len(urls)), flush=True) has_tested_projects = False @@ -40,7 +37,7 @@ def get_sitemap_text(client): # # ... # - res: Response = client.get("/sitemap.xml") - text = res.data.decode("utf-8") + res: Response = client.get('/sitemap.xml') + text = res.data.decode('utf-8') text = text.replace('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"', '') return text diff --git a/app/ch14_testing/final/tests/test_client.py b/app/ch14_testing/final/tests/test_client.py index ffbea486..4390409a 100644 --- a/app/ch14_testing/final/tests/test_client.py +++ b/app/ch14_testing/final/tests/test_client.py @@ -4,9 +4,7 @@ import sys import os -container_folder = os.path.abspath(os.path.join( - os.path.dirname(__file__), '..' -)) +container_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, container_folder) import pypi_org.app diff --git a/app/ch14_testing/starter/.idea/dataSources.local.xml b/app/ch14_testing/starter/.idea/dataSources.local.xml index 94233bb2..7a996036 100644 --- a/app/ch14_testing/starter/.idea/dataSources.local.xml +++ b/app/ch14_testing/starter/.idea/dataSources.local.xml @@ -6,8 +6,12 @@ " - false - *:@ + no-auth + + + + + \ No newline at end of file diff --git a/app/ch14_testing/starter/alembic/alembic_helpers.py b/app/ch14_testing/starter/alembic/alembic_helpers.py index 561f7a70..6aea52d4 100644 --- a/app/ch14_testing/starter/alembic/alembic_helpers.py +++ b/app/ch14_testing/starter/alembic/alembic_helpers.py @@ -5,8 +5,7 @@ def table_has_column(table, column): config = op.get_context().config - engine = engine_from_config( - config.get_section(config.config_ini_section), prefix='sqlalchemy.') + engine = engine_from_config(config.get_section(config.config_ini_section), prefix='sqlalchemy.') insp = reflection.Inspector.from_engine(engine) has_column = False for col in insp.get_columns(table): diff --git a/app/ch14_testing/starter/alembic/env.py b/app/ch14_testing/starter/alembic/env.py index 27ec49fb..1fdc9cab 100644 --- a/app/ch14_testing/starter/alembic/env.py +++ b/app/ch14_testing/starter/alembic/env.py @@ -26,6 +26,7 @@ sys.path.insert(0, folder) from pypi_org.data.modelbase import SqlAlchemyBase + # noinspection PyUnresolvedReferences import pypi_org.data.__all_models @@ -49,10 +50,8 @@ def run_migrations_offline(): script output. """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, target_metadata=target_metadata, literal_binds=True - ) + url = config.get_main_option('sqlalchemy.url') + context.configure(url=url, target_metadata=target_metadata, literal_binds=True) with context.begin_transaction(): context.run_migrations() @@ -67,14 +66,12 @@ def run_migrations_online(): """ connectable = engine_from_config( config.get_section(config.config_ini_section), - prefix="sqlalchemy.", + prefix='sqlalchemy.', poolclass=pool.NullPool, ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() diff --git a/app/ch14_testing/starter/alembic/versions/722c82f0097c_added_auditing_table.py b/app/ch14_testing/starter/alembic/versions/722c82f0097c_added_auditing_table.py index 2c3e2a39..7c6e892f 100644 --- a/app/ch14_testing/starter/alembic/versions/722c82f0097c_added_auditing_table.py +++ b/app/ch14_testing/starter/alembic/versions/722c82f0097c_added_auditing_table.py @@ -18,11 +18,12 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('auditing', - sa.Column('id', sa.String(), nullable=False), - sa.Column('created_date', sa.DateTime(), nullable=True), - sa.Column('description', sa.String(), nullable=True), - sa.PrimaryKeyConstraint('id') + op.create_table( + 'auditing', + sa.Column('id', sa.String(), nullable=False), + sa.Column('created_date', sa.DateTime(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id'), ) op.create_index(op.f('ix_auditing_created_date'), 'auditing', ['created_date'], unique=False) # ### end Alembic commands ### diff --git a/app/ch14_testing/starter/pypi_org/app.py b/app/ch14_testing/starter/pypi_org/app.py index 46c84acc..0aa9e391 100644 --- a/app/ch14_testing/starter/pypi_org/app.py +++ b/app/ch14_testing/starter/pypi_org/app.py @@ -2,6 +2,7 @@ import sys import flask + folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, folder) import pypi_org.data.db_session as db_session @@ -16,10 +17,7 @@ def main(): def setup_db(): - db_file = os.path.join( - os.path.dirname(__file__), - 'db', - 'pypi.sqlite') + db_file = os.path.join(os.path.dirname(__file__), 'db', 'pypi.sqlite') db_session.global_init(db_file) diff --git a/app/ch14_testing/starter/pypi_org/bin/basic_inserts.py b/app/ch14_testing/starter/pypi_org/bin/basic_inserts.py index 3a2f360d..6d04f551 100644 --- a/app/ch14_testing/starter/pypi_org/bin/basic_inserts.py +++ b/app/ch14_testing/starter/pypi_org/bin/basic_inserts.py @@ -1,14 +1,13 @@ import os import sys +# Make it run more easily outside of PyCharm +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + import pypi_org.data.db_session as db_session from pypi_org.data.package import Package from pypi_org.data.releases import Release -# Make it run more easily outside of PyCharm -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) - def main(): init_db() @@ -20,24 +19,24 @@ def insert_a_package(): p = Package() p.id = input('Package id / name: ').strip().lower() - p.summary = input("Package summary: ").strip() - p.author_name = input("Author: ").strip() - p.license = input("License: ").strip() + p.summary = input('Package summary: ').strip() + p.author_name = input('Author: ').strip() + p.license = input('License: ').strip() - print("Release 1:") + print('Release 1:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) - print("Release 2:") + print('Release 2:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) session = db_session.create_session() diff --git a/app/ch14_testing/starter/pypi_org/bin/load_data.py b/app/ch14_testing/starter/pypi_org/bin/load_data.py index 15237f18..3af7a03d 100644 --- a/app/ch14_testing/starter/pypi_org/bin/load_data.py +++ b/app/ch14_testing/starter/pypi_org/bin/load_data.py @@ -7,9 +7,9 @@ import progressbar from dateutil.parser import parse -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) +from pypi_org.infrastructure.num_convert import try_int import pypi_org.data.db_session as db_session from pypi_org.data.languages import ProgrammingLanguage from pypi_org.data.licenses import License @@ -39,7 +39,7 @@ def main(): def do_import_languages(file_data: List[dict]): imported = set() - print("Importing languages ... ", flush=True) + print('Importing languages ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -74,7 +74,7 @@ def do_import_languages(file_data: List[dict]): def do_import_licenses(file_data: List[dict]): imported = set() - print("Importing licenses ... ", flush=True) + print('Importing licenses ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -100,17 +100,17 @@ def do_import_licenses(file_data: List[dict]): def do_summary(): session = db_session.create_session() - print("Final numbers:") - print("Users: {:,}".format(session.query(User).count())) - print("Packages: {:,}".format(session.query(Package).count())) - print("Releases: {:,}".format(session.query(Release).count())) - print("Maintainers: {:,}".format(session.query(Maintainer).count())) - print("Languages: {:,}".format(session.query(ProgrammingLanguage).count())) - print("Licenses: {:,}".format(session.query(License).count())) + print('Final numbers:') + print('Users: {:,}'.format(session.query(User).count())) + print('Packages: {:,}'.format(session.query(Package).count())) + print('Releases: {:,}'.format(session.query(Release).count())) + print('Maintainers: {:,}'.format(session.query(Maintainer).count())) + print('Languages: {:,}'.format(session.query(ProgrammingLanguage).count())) + print('Licenses: {:,}'.format(session.query(License).count())) def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: - print("Importing users ... ", flush=True) + print('Importing users ... ', flush=True) with progressbar.ProgressBar(max_value=len(user_lookup)) as bar: for idx, (email, name) in enumerate(user_lookup.items()): session = db_session.create_session() @@ -134,29 +134,29 @@ def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: def do_import_packages(file_data: List[dict], user_lookup: Dict[str, User]): errored_packages = [] - print("Importing packages and releases ... ", flush=True) + print('Importing packages and releases ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): try: load_package(p, user_lookup) bar.update(idx) except Exception as x: - errored_packages.append((p, " *** Errored out for package {}, {}".format(p.get('package_name'), x))) + errored_packages.append((p, ' *** Errored out for package {}, {}'.format(p.get('package_name'), x))) raise sys.stderr.flush() sys.stdout.flush() print() - print("Completed packages with {} errors.".format(len(errored_packages))) - for (p, txt) in errored_packages: + print('Completed packages with {} errors.'.format(len(errored_packages))) + for p, txt in errored_packages: print(txt) def do_load_files() -> List[dict]: data_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../../data/pypi-top-100')) - print("Loading files from {}".format(data_path)) + print('Loading files from {}'.format(data_path)) files = get_file_names(data_path) - print("Found {:,} files, loading ...".format(len(files)), flush=True) - time.sleep(.1) + print('Found {:,} files, loading ...'.format(len(files)), flush=True) + time.sleep(0.1) file_data = [] with progressbar.ProgressBar(max_value=len(files)) as bar: @@ -171,7 +171,7 @@ def do_load_files() -> List[dict]: def find_users(data: List[dict]) -> dict: - print("Discovering users...", flush=True) + print('Discovering users...', flush=True) found_users = {} with progressbar.ProgressBar(max_value=len(data)) as bar: @@ -184,7 +184,7 @@ def find_users(data: List[dict]) -> dict: sys.stderr.flush() sys.stdout.flush() print() - print("Discovered {:,} users".format(len(found_users))) + print('Discovered {:,} users'.format(len(found_users))) print() return found_users @@ -215,7 +215,7 @@ def load_file_data(filename: str) -> dict: with open(filename, 'r', encoding='utf-8') as fin: data = json.load(fin) except Exception as x: - print("ERROR in file: {}, details: {}".format(filename, x), flush=True) + print('ERROR in file: {}, details: {}'.format(filename, x), flush=True) raise return data @@ -233,7 +233,7 @@ def load_package(data: dict, user_lookup: Dict[str, User]): p.author = info.get('author') p.author_email = info.get('author_email') - releases = build_releases(p.id, data.get("releases", {})) + releases = build_releases(p.id, data.get('releases', {})) if releases: p.created_date = releases[0].created_date @@ -283,18 +283,13 @@ def detect_license(license_text: str) -> Optional[str]: license_text = license_text.strip() if len(license_text) > 100 or '\n' in license_text: - return "CUSTOM" + return 'CUSTOM' - license_text = license_text \ - .replace('Software License', '') \ - .replace('License', '') + license_text = license_text.replace('Software License', '').replace('License', '') if '::' in license_text: # E.g. 'License :: OSI Approved :: Apache Software License' - return license_text \ - .split(':')[-1] \ - .replace(' ', ' ') \ - .strip() + return license_text.split(':')[-1].replace(' ', ' ').strip() return license_text.strip() @@ -339,13 +334,6 @@ def make_version_num(version_text): return major, minor, build -def try_int(text) -> int: - try: - return int(text) - except: - return 0 - - def init_db(): top_folder = os.path.dirname(__file__) rel_file = os.path.join('..', 'db', 'pypi.sqlite') @@ -357,9 +345,7 @@ def get_file_names(data_path: str) -> List[str]: files = [] for f in os.listdir(data_path): if f.endswith('.json'): - files.append( - os.path.abspath(os.path.join(data_path, f)) - ) + files.append(os.path.abspath(os.path.join(data_path, f))) files.sort() return files diff --git a/app/ch14_testing/starter/pypi_org/data/__all_models.py b/app/ch14_testing/starter/pypi_org/data/__all_models.py index f399ef54..ca4804ac 100644 --- a/app/ch14_testing/starter/pypi_org/data/__all_models.py +++ b/app/ch14_testing/starter/pypi_org/data/__all_models.py @@ -5,17 +5,24 @@ # noinspection PyUnresolvedReferences import pypi_org.data.audit + # noinspection PyUnresolvedReferences import pypi_org.data.downloads + # noinspection PyUnresolvedReferences import pypi_org.data.languages + # noinspection PyUnresolvedReferences import pypi_org.data.licenses + # noinspection PyUnresolvedReferences import pypi_org.data.maintainers + # noinspection PyUnresolvedReferences import pypi_org.data.package + # noinspection PyUnresolvedReferences import pypi_org.data.releases + # noinspection PyUnresolvedReferences import pypi_org.data.users diff --git a/app/ch14_testing/starter/pypi_org/data/db_session.py b/app/ch14_testing/starter/pypi_org/data/db_session.py index acfc485d..86eb3fb2 100644 --- a/app/ch14_testing/starter/pypi_org/data/db_session.py +++ b/app/ch14_testing/starter/pypi_org/data/db_session.py @@ -14,12 +14,15 @@ def global_init(db_file: str): return if not db_file or not db_file.strip(): - raise Exception("You must specify a db file.") + raise Exception('You must specify a db file.') conn_str = 'sqlite:///' + db_file.strip() - print("Connecting to DB with {}".format(conn_str)) + print('Connecting to DB with {}'.format(conn_str)) - engine = sa.create_engine(conn_str, echo=False) + # Adding check_same_thread = False after the recording. This can be an issue about + # creating / owner thread when cleaning up sessions, etc. This is a sqlite restriction + # that we probably don't care about in this example. + engine = sa.create_engine(conn_str, echo=False, connect_args={'check_same_thread': False}) __factory = orm.sessionmaker(bind=engine) # noinspection PyUnresolvedReferences diff --git a/app/ch14_testing/starter/pypi_org/data/downloads.py b/app/ch14_testing/starter/pypi_org/data/downloads.py index 66068f02..dd8afd65 100644 --- a/app/ch14_testing/starter/pypi_org/data/downloads.py +++ b/app/ch14_testing/starter/pypi_org/data/downloads.py @@ -7,8 +7,7 @@ class Download(SqlAlchemyBase): __tablename__ = 'downloads' id: int = sqlalchemy.Column(sqlalchemy.BigInteger, primary_key=True, autoincrement=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) package_id: str = sqlalchemy.Column(sqlalchemy.String, index=True) release_id: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) diff --git a/app/ch14_testing/starter/pypi_org/data/languages.py b/app/ch14_testing/starter/pypi_org/data/languages.py index 964b8444..3a73dde0 100644 --- a/app/ch14_testing/starter/pypi_org/data/languages.py +++ b/app/ch14_testing/starter/pypi_org/data/languages.py @@ -7,6 +7,5 @@ class ProgrammingLanguage(SqlAlchemyBase): __tablename__ = 'languages' id: str = sqlalchemy.Column(sqlalchemy.String, primary_key=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) description: str = sqlalchemy.Column(sqlalchemy.String) diff --git a/app/ch14_testing/starter/pypi_org/data/package.py b/app/ch14_testing/starter/pypi_org/data/package.py index c3420d1b..e0e48878 100644 --- a/app/ch14_testing/starter/pypi_org/data/package.py +++ b/app/ch14_testing/starter/pypi_org/data/package.py @@ -3,6 +3,8 @@ import sqlalchemy as sa import sqlalchemy.orm as orm +from sqlalchemy.orm import Mapped + from pypi_org.data.modelbase import SqlAlchemyBase from pypi_org.data.releases import Release @@ -12,7 +14,6 @@ class Package(SqlAlchemyBase): id: str = sa.Column(sa.String, primary_key=True) created_date: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) - last_updated: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) summary: str = sa.Column(sa.String, nullable=False) description: str = sa.Column(sa.String, nullable=True) @@ -26,11 +27,15 @@ class Package(SqlAlchemyBase): license: str = sa.Column(sa.String, index=True) # releases relationship - releases: List[Release] = orm.relation("Release", order_by=[ - Release.major_ver.desc(), - Release.minor_ver.desc(), - Release.build_ver.desc(), - ], back_populates='package') + releases: Mapped[Release] = orm.relationship( + 'Release', + order_by=[ + Release.major_ver.desc(), + Release.minor_ver.desc(), + Release.build_ver.desc(), + ], + back_populates='package', + ) def __repr__(self): return ''.format(self.id) diff --git a/app/ch14_testing/starter/pypi_org/data/releases.py b/app/ch14_testing/starter/pypi_org/data/releases.py index 7650e438..4b2db2bf 100644 --- a/app/ch14_testing/starter/pypi_org/data/releases.py +++ b/app/ch14_testing/starter/pypi_org/data/releases.py @@ -13,16 +13,14 @@ class Release(SqlAlchemyBase): minor_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) build_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) - created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, - default=datetime.datetime.now, - index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) comment: str = sqlalchemy.Column(sqlalchemy.String) url: str = sqlalchemy.Column(sqlalchemy.String) size: int = sqlalchemy.Column(sqlalchemy.BigInteger) # Package relationship - package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey("packages.id")) - package = orm.relation('Package') + package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey('packages.id')) + package = orm.relationship('Package') @property def version_text(self): diff --git a/app/ch14_testing/starter/pypi_org/infrastructure/cookie_auth.py b/app/ch14_testing/starter/pypi_org/infrastructure/cookie_auth.py index 64dc85a7..c215c0af 100644 --- a/app/ch14_testing/starter/pypi_org/infrastructure/cookie_auth.py +++ b/app/ch14_testing/starter/pypi_org/infrastructure/cookie_auth.py @@ -1,19 +1,18 @@ import hashlib -from datetime import timedelta from typing import Optional from flask import Request from flask import Response -from pypi_org.bin.load_data import try_int +from pypi_org.infrastructure.num_convert import try_int auth_cookie_name = 'pypi_demo_user' def set_auth(response: Response, user_id: int): hash_val = __hash_text(str(user_id)) - val = "{}:{}".format(user_id, hash_val) - response.set_cookie(auth_cookie_name, val) + val = '{}:{}'.format(user_id, hash_val) + response.set_cookie(auth_cookie_name, val, secure=False, httponly=True, samesite='Lax') def __hash_text(text: str) -> str: @@ -21,10 +20,6 @@ def __hash_text(text: str) -> str: return hashlib.sha512(text.encode('utf-8')).hexdigest() -def __add_cookie_callback(_, response: Response, name: str, value: str): - response.set_cookie(name, value, max_age=timedelta(days=30)) - - def get_user_id_via_auth_cookie(request: Request) -> Optional[int]: if auth_cookie_name not in request.cookies: return None @@ -38,7 +33,7 @@ def get_user_id_via_auth_cookie(request: Request) -> Optional[int]: hash_val = parts[1] hash_val_check = __hash_text(user_id) if hash_val != hash_val_check: - print("Warning: Hash mismatch, invalid cookie value") + print('Warning: Hash mismatch, invalid cookie value') return None return try_int(user_id) diff --git a/app/ch14_testing/starter/pypi_org/infrastructure/num_convert.py b/app/ch14_testing/starter/pypi_org/infrastructure/num_convert.py new file mode 100644 index 00000000..4e1d8879 --- /dev/null +++ b/app/ch14_testing/starter/pypi_org/infrastructure/num_convert.py @@ -0,0 +1,8 @@ +from typing import Optional + + +def try_int(text) -> Optional[int]: + try: + return int(text) + except: + return None diff --git a/app/ch14_testing/starter/pypi_org/infrastructure/request_dict.py b/app/ch14_testing/starter/pypi_org/infrastructure/request_dict.py index 495fd979..d44ae574 100644 --- a/app/ch14_testing/starter/pypi_org/infrastructure/request_dict.py +++ b/app/ch14_testing/starter/pypi_org/infrastructure/request_dict.py @@ -1,4 +1,5 @@ import flask +from werkzeug.datastructures import MultiDict class RequestDictionary(dict): @@ -13,11 +14,22 @@ def __getattr__(self, key): def create(default_val=None, **route_args) -> RequestDictionary: request = flask.request + # Adding this retro actively. Some folks are experiencing issues where they + # are getting a list rather than plain dict. I think it's from multiple + # entries in the multidict. This should fix it. + args = request.args + if isinstance(request.args, MultiDict): + args = request.args.to_dict() + + form = request.form + if isinstance(request.args, MultiDict): + form = request.form.to_dict() + data = { - **request.args, # The key/value pairs in the URL query string + **args, # The key/value pairs in the URL query string **request.headers, # Header values - **request.form, # The key/value pairs in the body, from a HTML post form - **route_args # And additional arguments the method access, if they want them merged. + **form, # The key/value pairs in the body, from a HTML post form + **route_args, # And additional arguments the method access, if they want them merged. } return RequestDictionary(data, default_val=default_val) diff --git a/app/ch14_testing/starter/pypi_org/infrastructure/view_modifiers.py b/app/ch14_testing/starter/pypi_org/infrastructure/view_modifiers.py index 82ce5baf..d944c142 100644 --- a/app/ch14_testing/starter/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch14_testing/starter/pypi_org/infrastructure/view_modifiers.py @@ -1,7 +1,8 @@ from functools import wraps import flask -import werkzeug.wrappers.response +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -11,9 +12,11 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) - if isinstance(response_val, flask.Response): + + if isinstance(response_val, werkzeug.wrappers.Response): return response_val - if isinstance(response_val, werkzeug.wrappers.response.Response): + + if isinstance(response_val, flask.Response): return response_val if isinstance(response_val, dict): @@ -23,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -39,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch14_testing/starter/pypi_org/services/package_service.py b/app/ch14_testing/starter/pypi_org/services/package_service.py index 71d4d02f..c2675f8d 100644 --- a/app/ch14_testing/starter/pypi_org/services/package_service.py +++ b/app/ch14_testing/starter/pypi_org/services/package_service.py @@ -9,12 +9,13 @@ def get_latest_releases(limit=10) -> List[Release]: session = db_session.create_session() try: - - releases = session.query(Release). \ - options(sqlalchemy.orm.joinedload(Release.package)). \ - order_by(Release.created_date.desc()). \ - limit(limit). \ - all() + releases = ( + session.query(Release) + .options(sqlalchemy.orm.joinedload(Release.package)) + .order_by(Release.created_date.desc()) + .limit(limit) + .all() + ) finally: session.close() @@ -46,11 +47,12 @@ def get_package_by_id(package_id: str) -> Optional[Package]: session = db_session.create_session() try: - - package = session.query(Package) \ - .options(sqlalchemy.orm.joinedload(Package.releases)) \ - .filter(Package.id == package_id) \ + package = ( + session.query(Package) + .options(sqlalchemy.orm.joinedload(Package.releases)) + .filter(Package.id == package_id) .first() + ) finally: session.close() diff --git a/app/ch14_testing/starter/pypi_org/viewmodels/cms/page_viewmodel.py b/app/ch14_testing/starter/pypi_org/viewmodels/cms/page_viewmodel.py index b7bb3249..ec033c3f 100644 --- a/app/ch14_testing/starter/pypi_org/viewmodels/cms/page_viewmodel.py +++ b/app/ch14_testing/starter/pypi_org/viewmodels/cms/page_viewmodel.py @@ -7,4 +7,3 @@ def __init__(self, full_url: str): super().__init__() self.page = cms_service.get_page(full_url) - diff --git a/app/ch14_testing/starter/pypi_org/viewmodels/packages/pagedetails_viewmodel.py b/app/ch14_testing/starter/pypi_org/viewmodels/packages/pagedetails_viewmodel.py index 63c4cf68..62bdca3d 100644 --- a/app/ch14_testing/starter/pypi_org/viewmodels/packages/pagedetails_viewmodel.py +++ b/app/ch14_testing/starter/pypi_org/viewmodels/packages/pagedetails_viewmodel.py @@ -11,12 +11,12 @@ def __init__(self, package_name: str): self.package_name = package_name.strip().lower() self.package = package_service.get_package_by_id(self.package_name) - self.latest_version = "0.0.0" + self.latest_version = '0.0.0' self.latest_release = None self.is_latest = True if self.package and self.package.releases: - self.latest_release = self.package.releases[0] + self.latest_release = self.package.releases self.latest_version = self.latest_release.version_text self.release_version = self.latest_release diff --git a/app/ch14_testing/starter/pypi_org/views/account_views.py b/app/ch14_testing/starter/pypi_org/views/account_views.py index 8697cc24..652f43b3 100644 --- a/app/ch14_testing/starter/pypi_org/views/account_views.py +++ b/app/ch14_testing/starter/pypi_org/views/account_views.py @@ -25,6 +25,7 @@ def index(): # ################### REGISTER ################################# + @blueprint.route('/account/register', methods=['GET']) @response(template_file='account/register.html') def register_get(): @@ -54,10 +55,16 @@ def register_post(): # ################### LOGIN ################################# + @blueprint.route('/account/login', methods=['GET']) @response(template_file='account/login.html') def login_get(): vm = LoginViewModel() + + # Added after recording, see https://github.com/talkpython/data-driven-web-apps-with-flask/issues/24 + if vm.user_id: + return flask.redirect('/account') + return vm.to_dict() @@ -72,7 +79,7 @@ def login_post(): user = user_service.login_user(vm.email, vm.password) if not user: - vm.error = "The account does not exist or the password is wrong." + vm.error = 'The account does not exist or the password is wrong.' return vm.to_dict() resp = flask.redirect('/account') @@ -83,6 +90,7 @@ def login_post(): # ################### LOGOUT ################################# + @blueprint.route('/account/logout') def logout(): resp = flask.redirect('/') diff --git a/app/ch14_testing/starter/pypi_org/views/package_views.py b/app/ch14_testing/starter/pypi_org/views/package_views.py index 1a74549f..2f862740 100644 --- a/app/ch14_testing/starter/pypi_org/views/package_views.py +++ b/app/ch14_testing/starter/pypi_org/views/package_views.py @@ -19,4 +19,4 @@ def package_details(package_name: str): @blueprint.route('/') def popular(rank: int): print(type(rank), rank) - return "The details for the {}th most popular package".format(rank) + return 'The details for the {}th most popular package'.format(rank) diff --git a/app/ch14_testing/starter/requirements.piptools b/app/ch14_testing/starter/requirements.piptools new file mode 100644 index 00000000..d13bf0c8 --- /dev/null +++ b/app/ch14_testing/starter/requirements.piptools @@ -0,0 +1,6 @@ +alembic +flask +passlib +progressbar2 +python-dateutil +sqlalchemy diff --git a/app/ch14_testing/starter/requirements.txt b/app/ch14_testing/starter/requirements.txt index e10973d8..19fc8c45 100644 --- a/app/ch14_testing/starter/requirements.txt +++ b/app/ch14_testing/starter/requirements.txt @@ -1,8 +1,42 @@ -werkzeug -flask -sqlalchemy - -progressbar2 -python-dateutil -passlib - +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +alembic==1.14.1 + # via -r requirements.piptools +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +mako==1.3.9 + # via alembic +markupsafe==3.0.2 + # via + # jinja2 + # mako + # werkzeug +passlib==1.7.4 + # via -r requirements.piptools +progressbar2==4.5.0 + # via -r requirements.piptools +python-dateutil==2.9.0.post0 + # via -r requirements.piptools +python-utils==3.9.1 + # via progressbar2 +six==1.17.0 + # via python-dateutil +sqlalchemy==2.0.38 + # via + # -r requirements.piptools + # alembic +typing-extensions==4.12.2 + # via + # alembic + # python-utils + # sqlalchemy +werkzeug==3.1.3 + # via flask diff --git a/app/ch15_deploy/final/.idea/flask-deploy.iml b/app/ch15_deploy/final/.idea/flask-deploy.iml index d0dc6825..1ac5a163 100644 --- a/app/ch15_deploy/final/.idea/flask-deploy.iml +++ b/app/ch15_deploy/final/.idea/flask-deploy.iml @@ -2,7 +2,7 @@ - + @@ -14,7 +14,4 @@ - - \ No newline at end of file diff --git a/app/ch15_deploy/final/.idea/misc.xml b/app/ch15_deploy/final/.idea/misc.xml index 349d87f1..2c526bdf 100644 --- a/app/ch15_deploy/final/.idea/misc.xml +++ b/app/ch15_deploy/final/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/app/ch15_deploy/final/alembic/alembic_helpers.py b/app/ch15_deploy/final/alembic/alembic_helpers.py index 561f7a70..6aea52d4 100644 --- a/app/ch15_deploy/final/alembic/alembic_helpers.py +++ b/app/ch15_deploy/final/alembic/alembic_helpers.py @@ -5,8 +5,7 @@ def table_has_column(table, column): config = op.get_context().config - engine = engine_from_config( - config.get_section(config.config_ini_section), prefix='sqlalchemy.') + engine = engine_from_config(config.get_section(config.config_ini_section), prefix='sqlalchemy.') insp = reflection.Inspector.from_engine(engine) has_column = False for col in insp.get_columns(table): diff --git a/app/ch15_deploy/final/alembic/env.py b/app/ch15_deploy/final/alembic/env.py index 27ec49fb..1fdc9cab 100644 --- a/app/ch15_deploy/final/alembic/env.py +++ b/app/ch15_deploy/final/alembic/env.py @@ -26,6 +26,7 @@ sys.path.insert(0, folder) from pypi_org.data.modelbase import SqlAlchemyBase + # noinspection PyUnresolvedReferences import pypi_org.data.__all_models @@ -49,10 +50,8 @@ def run_migrations_offline(): script output. """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, target_metadata=target_metadata, literal_binds=True - ) + url = config.get_main_option('sqlalchemy.url') + context.configure(url=url, target_metadata=target_metadata, literal_binds=True) with context.begin_transaction(): context.run_migrations() @@ -67,14 +66,12 @@ def run_migrations_online(): """ connectable = engine_from_config( config.get_section(config.config_ini_section), - prefix="sqlalchemy.", + prefix='sqlalchemy.', poolclass=pool.NullPool, ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() diff --git a/app/ch15_deploy/final/alembic/versions/722c82f0097c_added_auditing_table.py b/app/ch15_deploy/final/alembic/versions/722c82f0097c_added_auditing_table.py index 2c3e2a39..7c6e892f 100644 --- a/app/ch15_deploy/final/alembic/versions/722c82f0097c_added_auditing_table.py +++ b/app/ch15_deploy/final/alembic/versions/722c82f0097c_added_auditing_table.py @@ -18,11 +18,12 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('auditing', - sa.Column('id', sa.String(), nullable=False), - sa.Column('created_date', sa.DateTime(), nullable=True), - sa.Column('description', sa.String(), nullable=True), - sa.PrimaryKeyConstraint('id') + op.create_table( + 'auditing', + sa.Column('id', sa.String(), nullable=False), + sa.Column('created_date', sa.DateTime(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id'), ) op.create_index(op.f('ix_auditing_created_date'), 'auditing', ['created_date'], unique=False) # ### end Alembic commands ### diff --git a/app/ch15_deploy/final/pypi_org/app.py b/app/ch15_deploy/final/pypi_org/app.py index d1de87c6..ee364816 100644 --- a/app/ch15_deploy/final/pypi_org/app.py +++ b/app/ch15_deploy/final/pypi_org/app.py @@ -16,21 +16,18 @@ def main(): def configure(): - print("Configuring Flask app:") + print('Configuring Flask app:') register_blueprints() - print("Registered blueprints") + print('Registered blueprints') setup_db() - print("DB setup completed.") - print("", flush=True) + print('DB setup completed.') + print('', flush=True) def setup_db(): - db_file = os.path.join( - os.path.dirname(__file__), - 'db', - 'pypi.sqlite') + db_file = os.path.join(os.path.dirname(__file__), 'db', 'pypi.sqlite') db_session.global_init(db_file) diff --git a/app/ch15_deploy/final/pypi_org/bin/basic_inserts.py b/app/ch15_deploy/final/pypi_org/bin/basic_inserts.py index 3a2f360d..6d04f551 100644 --- a/app/ch15_deploy/final/pypi_org/bin/basic_inserts.py +++ b/app/ch15_deploy/final/pypi_org/bin/basic_inserts.py @@ -1,14 +1,13 @@ import os import sys +# Make it run more easily outside of PyCharm +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + import pypi_org.data.db_session as db_session from pypi_org.data.package import Package from pypi_org.data.releases import Release -# Make it run more easily outside of PyCharm -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) - def main(): init_db() @@ -20,24 +19,24 @@ def insert_a_package(): p = Package() p.id = input('Package id / name: ').strip().lower() - p.summary = input("Package summary: ").strip() - p.author_name = input("Author: ").strip() - p.license = input("License: ").strip() + p.summary = input('Package summary: ').strip() + p.author_name = input('Author: ').strip() + p.license = input('License: ').strip() - print("Release 1:") + print('Release 1:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) - print("Release 2:") + print('Release 2:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) session = db_session.create_session() diff --git a/app/ch15_deploy/final/pypi_org/bin/load_data.py b/app/ch15_deploy/final/pypi_org/bin/load_data.py index 15237f18..3af7a03d 100644 --- a/app/ch15_deploy/final/pypi_org/bin/load_data.py +++ b/app/ch15_deploy/final/pypi_org/bin/load_data.py @@ -7,9 +7,9 @@ import progressbar from dateutil.parser import parse -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) +from pypi_org.infrastructure.num_convert import try_int import pypi_org.data.db_session as db_session from pypi_org.data.languages import ProgrammingLanguage from pypi_org.data.licenses import License @@ -39,7 +39,7 @@ def main(): def do_import_languages(file_data: List[dict]): imported = set() - print("Importing languages ... ", flush=True) + print('Importing languages ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -74,7 +74,7 @@ def do_import_languages(file_data: List[dict]): def do_import_licenses(file_data: List[dict]): imported = set() - print("Importing licenses ... ", flush=True) + print('Importing licenses ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -100,17 +100,17 @@ def do_import_licenses(file_data: List[dict]): def do_summary(): session = db_session.create_session() - print("Final numbers:") - print("Users: {:,}".format(session.query(User).count())) - print("Packages: {:,}".format(session.query(Package).count())) - print("Releases: {:,}".format(session.query(Release).count())) - print("Maintainers: {:,}".format(session.query(Maintainer).count())) - print("Languages: {:,}".format(session.query(ProgrammingLanguage).count())) - print("Licenses: {:,}".format(session.query(License).count())) + print('Final numbers:') + print('Users: {:,}'.format(session.query(User).count())) + print('Packages: {:,}'.format(session.query(Package).count())) + print('Releases: {:,}'.format(session.query(Release).count())) + print('Maintainers: {:,}'.format(session.query(Maintainer).count())) + print('Languages: {:,}'.format(session.query(ProgrammingLanguage).count())) + print('Licenses: {:,}'.format(session.query(License).count())) def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: - print("Importing users ... ", flush=True) + print('Importing users ... ', flush=True) with progressbar.ProgressBar(max_value=len(user_lookup)) as bar: for idx, (email, name) in enumerate(user_lookup.items()): session = db_session.create_session() @@ -134,29 +134,29 @@ def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: def do_import_packages(file_data: List[dict], user_lookup: Dict[str, User]): errored_packages = [] - print("Importing packages and releases ... ", flush=True) + print('Importing packages and releases ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): try: load_package(p, user_lookup) bar.update(idx) except Exception as x: - errored_packages.append((p, " *** Errored out for package {}, {}".format(p.get('package_name'), x))) + errored_packages.append((p, ' *** Errored out for package {}, {}'.format(p.get('package_name'), x))) raise sys.stderr.flush() sys.stdout.flush() print() - print("Completed packages with {} errors.".format(len(errored_packages))) - for (p, txt) in errored_packages: + print('Completed packages with {} errors.'.format(len(errored_packages))) + for p, txt in errored_packages: print(txt) def do_load_files() -> List[dict]: data_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../../data/pypi-top-100')) - print("Loading files from {}".format(data_path)) + print('Loading files from {}'.format(data_path)) files = get_file_names(data_path) - print("Found {:,} files, loading ...".format(len(files)), flush=True) - time.sleep(.1) + print('Found {:,} files, loading ...'.format(len(files)), flush=True) + time.sleep(0.1) file_data = [] with progressbar.ProgressBar(max_value=len(files)) as bar: @@ -171,7 +171,7 @@ def do_load_files() -> List[dict]: def find_users(data: List[dict]) -> dict: - print("Discovering users...", flush=True) + print('Discovering users...', flush=True) found_users = {} with progressbar.ProgressBar(max_value=len(data)) as bar: @@ -184,7 +184,7 @@ def find_users(data: List[dict]) -> dict: sys.stderr.flush() sys.stdout.flush() print() - print("Discovered {:,} users".format(len(found_users))) + print('Discovered {:,} users'.format(len(found_users))) print() return found_users @@ -215,7 +215,7 @@ def load_file_data(filename: str) -> dict: with open(filename, 'r', encoding='utf-8') as fin: data = json.load(fin) except Exception as x: - print("ERROR in file: {}, details: {}".format(filename, x), flush=True) + print('ERROR in file: {}, details: {}'.format(filename, x), flush=True) raise return data @@ -233,7 +233,7 @@ def load_package(data: dict, user_lookup: Dict[str, User]): p.author = info.get('author') p.author_email = info.get('author_email') - releases = build_releases(p.id, data.get("releases", {})) + releases = build_releases(p.id, data.get('releases', {})) if releases: p.created_date = releases[0].created_date @@ -283,18 +283,13 @@ def detect_license(license_text: str) -> Optional[str]: license_text = license_text.strip() if len(license_text) > 100 or '\n' in license_text: - return "CUSTOM" + return 'CUSTOM' - license_text = license_text \ - .replace('Software License', '') \ - .replace('License', '') + license_text = license_text.replace('Software License', '').replace('License', '') if '::' in license_text: # E.g. 'License :: OSI Approved :: Apache Software License' - return license_text \ - .split(':')[-1] \ - .replace(' ', ' ') \ - .strip() + return license_text.split(':')[-1].replace(' ', ' ').strip() return license_text.strip() @@ -339,13 +334,6 @@ def make_version_num(version_text): return major, minor, build -def try_int(text) -> int: - try: - return int(text) - except: - return 0 - - def init_db(): top_folder = os.path.dirname(__file__) rel_file = os.path.join('..', 'db', 'pypi.sqlite') @@ -357,9 +345,7 @@ def get_file_names(data_path: str) -> List[str]: files = [] for f in os.listdir(data_path): if f.endswith('.json'): - files.append( - os.path.abspath(os.path.join(data_path, f)) - ) + files.append(os.path.abspath(os.path.join(data_path, f))) files.sort() return files diff --git a/app/ch15_deploy/final/pypi_org/data/__all_models.py b/app/ch15_deploy/final/pypi_org/data/__all_models.py index f399ef54..ca4804ac 100644 --- a/app/ch15_deploy/final/pypi_org/data/__all_models.py +++ b/app/ch15_deploy/final/pypi_org/data/__all_models.py @@ -5,17 +5,24 @@ # noinspection PyUnresolvedReferences import pypi_org.data.audit + # noinspection PyUnresolvedReferences import pypi_org.data.downloads + # noinspection PyUnresolvedReferences import pypi_org.data.languages + # noinspection PyUnresolvedReferences import pypi_org.data.licenses + # noinspection PyUnresolvedReferences import pypi_org.data.maintainers + # noinspection PyUnresolvedReferences import pypi_org.data.package + # noinspection PyUnresolvedReferences import pypi_org.data.releases + # noinspection PyUnresolvedReferences import pypi_org.data.users diff --git a/app/ch15_deploy/final/pypi_org/data/db_session.py b/app/ch15_deploy/final/pypi_org/data/db_session.py index acfc485d..86eb3fb2 100644 --- a/app/ch15_deploy/final/pypi_org/data/db_session.py +++ b/app/ch15_deploy/final/pypi_org/data/db_session.py @@ -14,12 +14,15 @@ def global_init(db_file: str): return if not db_file or not db_file.strip(): - raise Exception("You must specify a db file.") + raise Exception('You must specify a db file.') conn_str = 'sqlite:///' + db_file.strip() - print("Connecting to DB with {}".format(conn_str)) + print('Connecting to DB with {}'.format(conn_str)) - engine = sa.create_engine(conn_str, echo=False) + # Adding check_same_thread = False after the recording. This can be an issue about + # creating / owner thread when cleaning up sessions, etc. This is a sqlite restriction + # that we probably don't care about in this example. + engine = sa.create_engine(conn_str, echo=False, connect_args={'check_same_thread': False}) __factory = orm.sessionmaker(bind=engine) # noinspection PyUnresolvedReferences diff --git a/app/ch15_deploy/final/pypi_org/data/downloads.py b/app/ch15_deploy/final/pypi_org/data/downloads.py index 66068f02..dd8afd65 100644 --- a/app/ch15_deploy/final/pypi_org/data/downloads.py +++ b/app/ch15_deploy/final/pypi_org/data/downloads.py @@ -7,8 +7,7 @@ class Download(SqlAlchemyBase): __tablename__ = 'downloads' id: int = sqlalchemy.Column(sqlalchemy.BigInteger, primary_key=True, autoincrement=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) package_id: str = sqlalchemy.Column(sqlalchemy.String, index=True) release_id: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) diff --git a/app/ch15_deploy/final/pypi_org/data/languages.py b/app/ch15_deploy/final/pypi_org/data/languages.py index 964b8444..3a73dde0 100644 --- a/app/ch15_deploy/final/pypi_org/data/languages.py +++ b/app/ch15_deploy/final/pypi_org/data/languages.py @@ -7,6 +7,5 @@ class ProgrammingLanguage(SqlAlchemyBase): __tablename__ = 'languages' id: str = sqlalchemy.Column(sqlalchemy.String, primary_key=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) description: str = sqlalchemy.Column(sqlalchemy.String) diff --git a/app/ch15_deploy/final/pypi_org/data/package.py b/app/ch15_deploy/final/pypi_org/data/package.py index c3420d1b..e0e48878 100644 --- a/app/ch15_deploy/final/pypi_org/data/package.py +++ b/app/ch15_deploy/final/pypi_org/data/package.py @@ -3,6 +3,8 @@ import sqlalchemy as sa import sqlalchemy.orm as orm +from sqlalchemy.orm import Mapped + from pypi_org.data.modelbase import SqlAlchemyBase from pypi_org.data.releases import Release @@ -12,7 +14,6 @@ class Package(SqlAlchemyBase): id: str = sa.Column(sa.String, primary_key=True) created_date: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) - last_updated: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) summary: str = sa.Column(sa.String, nullable=False) description: str = sa.Column(sa.String, nullable=True) @@ -26,11 +27,15 @@ class Package(SqlAlchemyBase): license: str = sa.Column(sa.String, index=True) # releases relationship - releases: List[Release] = orm.relation("Release", order_by=[ - Release.major_ver.desc(), - Release.minor_ver.desc(), - Release.build_ver.desc(), - ], back_populates='package') + releases: Mapped[Release] = orm.relationship( + 'Release', + order_by=[ + Release.major_ver.desc(), + Release.minor_ver.desc(), + Release.build_ver.desc(), + ], + back_populates='package', + ) def __repr__(self): return ''.format(self.id) diff --git a/app/ch15_deploy/final/pypi_org/data/releases.py b/app/ch15_deploy/final/pypi_org/data/releases.py index 7650e438..4b2db2bf 100644 --- a/app/ch15_deploy/final/pypi_org/data/releases.py +++ b/app/ch15_deploy/final/pypi_org/data/releases.py @@ -13,16 +13,14 @@ class Release(SqlAlchemyBase): minor_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) build_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) - created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, - default=datetime.datetime.now, - index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) comment: str = sqlalchemy.Column(sqlalchemy.String) url: str = sqlalchemy.Column(sqlalchemy.String) size: int = sqlalchemy.Column(sqlalchemy.BigInteger) # Package relationship - package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey("packages.id")) - package = orm.relation('Package') + package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey('packages.id')) + package = orm.relationship('Package') @property def version_text(self): diff --git a/app/ch15_deploy/final/pypi_org/infrastructure/cookie_auth.py b/app/ch15_deploy/final/pypi_org/infrastructure/cookie_auth.py index 64dc85a7..c215c0af 100644 --- a/app/ch15_deploy/final/pypi_org/infrastructure/cookie_auth.py +++ b/app/ch15_deploy/final/pypi_org/infrastructure/cookie_auth.py @@ -1,19 +1,18 @@ import hashlib -from datetime import timedelta from typing import Optional from flask import Request from flask import Response -from pypi_org.bin.load_data import try_int +from pypi_org.infrastructure.num_convert import try_int auth_cookie_name = 'pypi_demo_user' def set_auth(response: Response, user_id: int): hash_val = __hash_text(str(user_id)) - val = "{}:{}".format(user_id, hash_val) - response.set_cookie(auth_cookie_name, val) + val = '{}:{}'.format(user_id, hash_val) + response.set_cookie(auth_cookie_name, val, secure=False, httponly=True, samesite='Lax') def __hash_text(text: str) -> str: @@ -21,10 +20,6 @@ def __hash_text(text: str) -> str: return hashlib.sha512(text.encode('utf-8')).hexdigest() -def __add_cookie_callback(_, response: Response, name: str, value: str): - response.set_cookie(name, value, max_age=timedelta(days=30)) - - def get_user_id_via_auth_cookie(request: Request) -> Optional[int]: if auth_cookie_name not in request.cookies: return None @@ -38,7 +33,7 @@ def get_user_id_via_auth_cookie(request: Request) -> Optional[int]: hash_val = parts[1] hash_val_check = __hash_text(user_id) if hash_val != hash_val_check: - print("Warning: Hash mismatch, invalid cookie value") + print('Warning: Hash mismatch, invalid cookie value') return None return try_int(user_id) diff --git a/app/ch15_deploy/final/pypi_org/infrastructure/num_convert.py b/app/ch15_deploy/final/pypi_org/infrastructure/num_convert.py new file mode 100644 index 00000000..4e1d8879 --- /dev/null +++ b/app/ch15_deploy/final/pypi_org/infrastructure/num_convert.py @@ -0,0 +1,8 @@ +from typing import Optional + + +def try_int(text) -> Optional[int]: + try: + return int(text) + except: + return None diff --git a/app/ch15_deploy/final/pypi_org/infrastructure/request_dict.py b/app/ch15_deploy/final/pypi_org/infrastructure/request_dict.py index 495fd979..d44ae574 100644 --- a/app/ch15_deploy/final/pypi_org/infrastructure/request_dict.py +++ b/app/ch15_deploy/final/pypi_org/infrastructure/request_dict.py @@ -1,4 +1,5 @@ import flask +from werkzeug.datastructures import MultiDict class RequestDictionary(dict): @@ -13,11 +14,22 @@ def __getattr__(self, key): def create(default_val=None, **route_args) -> RequestDictionary: request = flask.request + # Adding this retro actively. Some folks are experiencing issues where they + # are getting a list rather than plain dict. I think it's from multiple + # entries in the multidict. This should fix it. + args = request.args + if isinstance(request.args, MultiDict): + args = request.args.to_dict() + + form = request.form + if isinstance(request.args, MultiDict): + form = request.form.to_dict() + data = { - **request.args, # The key/value pairs in the URL query string + **args, # The key/value pairs in the URL query string **request.headers, # Header values - **request.form, # The key/value pairs in the body, from a HTML post form - **route_args # And additional arguments the method access, if they want them merged. + **form, # The key/value pairs in the body, from a HTML post form + **route_args, # And additional arguments the method access, if they want them merged. } return RequestDictionary(data, default_val=default_val) diff --git a/app/ch15_deploy/final/pypi_org/infrastructure/view_modifiers.py b/app/ch15_deploy/final/pypi_org/infrastructure/view_modifiers.py index 82ce5baf..d944c142 100644 --- a/app/ch15_deploy/final/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch15_deploy/final/pypi_org/infrastructure/view_modifiers.py @@ -1,7 +1,8 @@ from functools import wraps import flask -import werkzeug.wrappers.response +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -11,9 +12,11 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) - if isinstance(response_val, flask.Response): + + if isinstance(response_val, werkzeug.wrappers.Response): return response_val - if isinstance(response_val, werkzeug.wrappers.response.Response): + + if isinstance(response_val, flask.Response): return response_val if isinstance(response_val, dict): @@ -23,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -39,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch15_deploy/final/pypi_org/services/package_service.py b/app/ch15_deploy/final/pypi_org/services/package_service.py index b03408d2..9a153773 100644 --- a/app/ch15_deploy/final/pypi_org/services/package_service.py +++ b/app/ch15_deploy/final/pypi_org/services/package_service.py @@ -10,12 +10,13 @@ def get_latest_releases(limit=10) -> List[Release]: session = db_session.create_session() try: - - releases = session.query(Release). \ - options(sqlalchemy.orm.joinedload(Release.package)). \ - order_by(Release.created_date.desc()). \ - limit(limit). \ - all() + releases = ( + session.query(Release) + .options(sqlalchemy.orm.joinedload(Release.package)) + .order_by(Release.created_date.desc()) + .limit(limit) + .all() + ) finally: session.close() @@ -47,11 +48,12 @@ def get_package_by_id(package_id: str) -> Optional[Package]: session = db_session.create_session() try: - - package = session.query(Package) \ - .options(sqlalchemy.orm.joinedload(Package.releases)) \ - .filter(Package.id == package_id) \ + package = ( + session.query(Package) + .options(sqlalchemy.orm.joinedload(Package.releases)) + .filter(Package.id == package_id) .first() + ) finally: session.close() diff --git a/app/ch15_deploy/final/pypi_org/viewmodels/cms/page_viewmodel.py b/app/ch15_deploy/final/pypi_org/viewmodels/cms/page_viewmodel.py index b7bb3249..ec033c3f 100644 --- a/app/ch15_deploy/final/pypi_org/viewmodels/cms/page_viewmodel.py +++ b/app/ch15_deploy/final/pypi_org/viewmodels/cms/page_viewmodel.py @@ -7,4 +7,3 @@ def __init__(self, full_url: str): super().__init__() self.page = cms_service.get_page(full_url) - diff --git a/app/ch15_deploy/final/pypi_org/viewmodels/packages/pagedetails_viewmodel.py b/app/ch15_deploy/final/pypi_org/viewmodels/packages/pagedetails_viewmodel.py index 63c4cf68..62bdca3d 100644 --- a/app/ch15_deploy/final/pypi_org/viewmodels/packages/pagedetails_viewmodel.py +++ b/app/ch15_deploy/final/pypi_org/viewmodels/packages/pagedetails_viewmodel.py @@ -11,12 +11,12 @@ def __init__(self, package_name: str): self.package_name = package_name.strip().lower() self.package = package_service.get_package_by_id(self.package_name) - self.latest_version = "0.0.0" + self.latest_version = '0.0.0' self.latest_release = None self.is_latest = True if self.package and self.package.releases: - self.latest_release = self.package.releases[0] + self.latest_release = self.package.releases self.latest_version = self.latest_release.version_text self.release_version = self.latest_release diff --git a/app/ch15_deploy/final/pypi_org/viewmodels/seo/sitemap_viewmodel.py b/app/ch15_deploy/final/pypi_org/viewmodels/seo/sitemap_viewmodel.py index 912c4df0..71df2f99 100644 --- a/app/ch15_deploy/final/pypi_org/viewmodels/seo/sitemap_viewmodel.py +++ b/app/ch15_deploy/final/pypi_org/viewmodels/seo/sitemap_viewmodel.py @@ -8,5 +8,5 @@ class SiteMapViewModel(ViewModelBase): def __init__(self, limit: int): super().__init__() self.packages = package_service.all_packages(limit) - self.last_updated_text = "2019-07-15" - self.site = "{}://{}".format(flask.request.scheme, flask.request.host) + self.last_updated_text = '2019-07-15' + self.site = '{}://{}'.format(flask.request.scheme, flask.request.host) diff --git a/app/ch15_deploy/final/pypi_org/views/account_views.py b/app/ch15_deploy/final/pypi_org/views/account_views.py index 8697cc24..261f9c34 100644 --- a/app/ch15_deploy/final/pypi_org/views/account_views.py +++ b/app/ch15_deploy/final/pypi_org/views/account_views.py @@ -1,8 +1,8 @@ import flask +import pypi_org.infrastructure.cookie_auth as cookie_auth from pypi_org.infrastructure.view_modifiers import response from pypi_org.services import user_service -import pypi_org.infrastructure.cookie_auth as cookie_auth from pypi_org.viewmodels.account.index_viewmodel import IndexViewModel from pypi_org.viewmodels.account.login_viewmodel import LoginViewModel from pypi_org.viewmodels.account.register_viewmodel import RegisterViewModel @@ -25,6 +25,7 @@ def index(): # ################### REGISTER ################################# + @blueprint.route('/account/register', methods=['GET']) @response(template_file='account/register.html') def register_get(): @@ -54,10 +55,16 @@ def register_post(): # ################### LOGIN ################################# + @blueprint.route('/account/login', methods=['GET']) @response(template_file='account/login.html') def login_get(): vm = LoginViewModel() + + # Added after recording, see https://github.com/talkpython/data-driven-web-apps-with-flask/issues/24 + if vm.user_id: + return flask.redirect('/account') + return vm.to_dict() @@ -72,7 +79,7 @@ def login_post(): user = user_service.login_user(vm.email, vm.password) if not user: - vm.error = "The account does not exist or the password is wrong." + vm.error = 'The account does not exist or the password is wrong.' return vm.to_dict() resp = flask.redirect('/account') @@ -83,6 +90,7 @@ def login_post(): # ################### LOGOUT ################################# + @blueprint.route('/account/logout') def logout(): resp = flask.redirect('/') diff --git a/app/ch15_deploy/final/pypi_org/views/package_views.py b/app/ch15_deploy/final/pypi_org/views/package_views.py index 1a74549f..2f862740 100644 --- a/app/ch15_deploy/final/pypi_org/views/package_views.py +++ b/app/ch15_deploy/final/pypi_org/views/package_views.py @@ -19,4 +19,4 @@ def package_details(package_name: str): @blueprint.route('/') def popular(rank: int): print(type(rank), rank) - return "The details for the {}th most popular package".format(rank) + return 'The details for the {}th most popular package'.format(rank) diff --git a/app/ch15_deploy/final/pypi_org/views/seo_view.py b/app/ch15_deploy/final/pypi_org/views/seo_view.py index 93c88de5..f86b6bff 100644 --- a/app/ch15_deploy/final/pypi_org/views/seo_view.py +++ b/app/ch15_deploy/final/pypi_org/views/seo_view.py @@ -18,6 +18,7 @@ def sitemap(): # ################### Robots ################################# + @blueprint.route('/robots.txt') @response(mimetype='text/plain', template_file='seo/robots.txt') def robots(): diff --git a/app/ch15_deploy/final/requirements.piptools b/app/ch15_deploy/final/requirements.piptools new file mode 100644 index 00000000..d13bf0c8 --- /dev/null +++ b/app/ch15_deploy/final/requirements.piptools @@ -0,0 +1,6 @@ +alembic +flask +passlib +progressbar2 +python-dateutil +sqlalchemy diff --git a/app/ch15_deploy/final/requirements.txt b/app/ch15_deploy/final/requirements.txt index e10973d8..19fc8c45 100644 --- a/app/ch15_deploy/final/requirements.txt +++ b/app/ch15_deploy/final/requirements.txt @@ -1,8 +1,42 @@ -werkzeug -flask -sqlalchemy - -progressbar2 -python-dateutil -passlib - +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +alembic==1.14.1 + # via -r requirements.piptools +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +mako==1.3.9 + # via alembic +markupsafe==3.0.2 + # via + # jinja2 + # mako + # werkzeug +passlib==1.7.4 + # via -r requirements.piptools +progressbar2==4.5.0 + # via -r requirements.piptools +python-dateutil==2.9.0.post0 + # via -r requirements.piptools +python-utils==3.9.1 + # via progressbar2 +six==1.17.0 + # via python-dateutil +sqlalchemy==2.0.38 + # via + # -r requirements.piptools + # alembic +typing-extensions==4.12.2 + # via + # alembic + # python-utils + # sqlalchemy +werkzeug==3.1.3 + # via flask diff --git a/app/ch15_deploy/final/server/server_setup.sh b/app/ch15_deploy/final/server/server_setup.sh index 16842e59..013ea4b4 100644 --- a/app/ch15_deploy/final/server/server_setup.sh +++ b/app/ch15_deploy/final/server/server_setup.sh @@ -74,9 +74,27 @@ update-rc.d nginx enable service nginx restart -# Optionally add SSL support via Let's Encrypt: -# https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-18-04 +# Optionally add SSL support via Let's Encrypt +# NOTE: These steps have changed since the recording. -add-apt-repository ppa:certbot/certbot -apt install python-certbot-nginx +####### NEW STEPS ############################################### +# See https://certbot.eff.org/instructions?ws=nginx&os=ubuntufocal&tab=standard + +# Because always a good idea :) +apt update +apt upgrade + +# Not need even though it's in the instructions, is installed on Ubuntu +# Skip -> install snapd https://snapcraft.io/docs/installing-snapd + +snap install --classic certbot +ln -s /snap/bin/certbot /usr/bin/certbot certbot --nginx -d fakepypi.talkpython.com + +####### THESE ARE THE OLD STEPS ################################# +# +## https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-18-04 +# +#add-apt-repository ppa:certbot/certbot +#apt install python-certbot-nginx +#certbot --nginx -d fakepypi.talkpython.com diff --git a/app/ch15_deploy/final/tests/_all_tests.py b/app/ch15_deploy/final/tests/_all_tests.py index daa69ea4..e5c9b9ea 100644 --- a/app/ch15_deploy/final/tests/_all_tests.py +++ b/app/ch15_deploy/final/tests/_all_tests.py @@ -1,17 +1,18 @@ import sys import os -container_folder = os.path.abspath(os.path.join( - os.path.dirname(__file__), '..' -)) +container_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, container_folder) # noinspection PyUnresolvedReferences from account_tests import * + # noinspection PyUnresolvedReferences from package_tests import * + # noinspection PyUnresolvedReferences from sitemap_tests import * + # noinspection PyUnresolvedReferences from home_tests import * diff --git a/app/ch15_deploy/final/tests/account_tests.py b/app/ch15_deploy/final/tests/account_tests.py index 4f5aa156..e31e8c28 100644 --- a/app/ch15_deploy/final/tests/account_tests.py +++ b/app/ch15_deploy/final/tests/account_tests.py @@ -7,7 +7,7 @@ def test_example(): - print("Test example...") + print('Test example...') assert 1 + 2 == 3 @@ -15,11 +15,7 @@ def test_vm_register_validation_when_valid(): # 3 A's of test: Arrange, Act, then Assert # Arrange - form_data = { - 'name': 'Michael', - 'email': 'michael@talkpython.fm', - 'password': 'a' * 6 - } + form_data = {'name': 'Michael', 'email': 'michael@talkpython.fm', 'password': 'a' * 6} with flask_app.test_request_context(path='/account/register', data=form_data): vm = RegisterViewModel() @@ -37,11 +33,7 @@ def test_vm_register_validation_for_existing_user(): # 3 A's of test: Arrange, Act, then Assert # Arrange - form_data = { - 'name': 'Michael', - 'email': 'michael@talkpython.fm', - 'password': 'a' * 6 - } + form_data = {'name': 'Michael', 'email': 'michael@talkpython.fm', 'password': 'a' * 6} with flask_app.test_request_context(path='/account/register', data=form_data): vm = RegisterViewModel() @@ -62,11 +54,8 @@ def test_v_register_view_new_user(): # Arrange from pypi_org.views.account_views import register_post - form_data = { - 'name': 'Michael', - 'email': 'michael@talkpython.fm', - 'password': 'a' * 6 - } + + form_data = {'name': 'Michael', 'email': 'michael@talkpython.fm', 'password': 'a' * 6} target = 'pypi_org.services.user_service.find_user_by_email' find_user = unittest.mock.patch(target, return_value=None) diff --git a/app/ch15_deploy/final/tests/package_tests.py b/app/ch15_deploy/final/tests/package_tests.py index efd04ef5..a7c221a5 100644 --- a/app/ch15_deploy/final/tests/package_tests.py +++ b/app/ch15_deploy/final/tests/package_tests.py @@ -12,15 +12,14 @@ def test_package_details_success(): test_package = Package() test_package.id = 'sqlalchemy' - test_package.description = "TDB" + test_package.description = 'TDB' test_package.releases = [ Release(created_date=datetime.datetime.now(), major_ver=1, minor_ver=2, build_ver=200), Release(created_date=datetime.datetime.now() - datetime.timedelta(days=10)), ] # Act - with unittest.mock.patch('pypi_org.services.package_service.get_package_by_id', - return_value=test_package): + with unittest.mock.patch('pypi_org.services.package_service.get_package_by_id', return_value=test_package): with flask_app.test_request_context(path='/project/' + test_package.id): resp: Response = package_details(test_package.id) @@ -33,8 +32,7 @@ def test_package_details_404(client): bad_package_url = 'sqlalchemy_missing' # Act - with unittest.mock.patch('pypi_org.services.package_service.get_package_by_id', - return_value=None): + with unittest.mock.patch('pypi_org.services.package_service.get_package_by_id', return_value=None): resp: Response = client.get(bad_package_url) assert resp.status_code == 404 diff --git a/app/ch15_deploy/final/tests/sitemap_tests.py b/app/ch15_deploy/final/tests/sitemap_tests.py index b1afe0fe..4494c30a 100644 --- a/app/ch15_deploy/final/tests/sitemap_tests.py +++ b/app/ch15_deploy/final/tests/sitemap_tests.py @@ -10,10 +10,7 @@ def test_int_site_mapped_urls(client): href.text.strip().replace('http://127.0.0.1:5000', '').replace('http://localhost', '') for href in list(x.findall('url/loc')) ] - urls = [ - u if u else '/' - for u in urls - ] + urls = [u if u else '/' for u in urls] print('Testing {} urls from sitemap...'.format(len(urls)), flush=True) has_tested_projects = False @@ -40,7 +37,7 @@ def get_sitemap_text(client): # # ... # - res: Response = client.get("/sitemap.xml") - text = res.data.decode("utf-8") + res: Response = client.get('/sitemap.xml') + text = res.data.decode('utf-8') text = text.replace('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"', '') return text diff --git a/app/ch15_deploy/final/tests/test_client.py b/app/ch15_deploy/final/tests/test_client.py index ffbea486..4390409a 100644 --- a/app/ch15_deploy/final/tests/test_client.py +++ b/app/ch15_deploy/final/tests/test_client.py @@ -4,9 +4,7 @@ import sys import os -container_folder = os.path.abspath(os.path.join( - os.path.dirname(__file__), '..' -)) +container_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, container_folder) import pypi_org.app diff --git a/app/ch15_deploy/starter/.idea/codeStyles/codeStyleConfig.xml b/app/ch15_deploy/starter/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..a55e7a17 --- /dev/null +++ b/app/ch15_deploy/starter/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/ch15_deploy/starter/alembic/alembic_helpers.py b/app/ch15_deploy/starter/alembic/alembic_helpers.py index 561f7a70..6aea52d4 100644 --- a/app/ch15_deploy/starter/alembic/alembic_helpers.py +++ b/app/ch15_deploy/starter/alembic/alembic_helpers.py @@ -5,8 +5,7 @@ def table_has_column(table, column): config = op.get_context().config - engine = engine_from_config( - config.get_section(config.config_ini_section), prefix='sqlalchemy.') + engine = engine_from_config(config.get_section(config.config_ini_section), prefix='sqlalchemy.') insp = reflection.Inspector.from_engine(engine) has_column = False for col in insp.get_columns(table): diff --git a/app/ch15_deploy/starter/alembic/env.py b/app/ch15_deploy/starter/alembic/env.py index 27ec49fb..1fdc9cab 100644 --- a/app/ch15_deploy/starter/alembic/env.py +++ b/app/ch15_deploy/starter/alembic/env.py @@ -26,6 +26,7 @@ sys.path.insert(0, folder) from pypi_org.data.modelbase import SqlAlchemyBase + # noinspection PyUnresolvedReferences import pypi_org.data.__all_models @@ -49,10 +50,8 @@ def run_migrations_offline(): script output. """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, target_metadata=target_metadata, literal_binds=True - ) + url = config.get_main_option('sqlalchemy.url') + context.configure(url=url, target_metadata=target_metadata, literal_binds=True) with context.begin_transaction(): context.run_migrations() @@ -67,14 +66,12 @@ def run_migrations_online(): """ connectable = engine_from_config( config.get_section(config.config_ini_section), - prefix="sqlalchemy.", + prefix='sqlalchemy.', poolclass=pool.NullPool, ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() diff --git a/app/ch15_deploy/starter/alembic/versions/722c82f0097c_added_auditing_table.py b/app/ch15_deploy/starter/alembic/versions/722c82f0097c_added_auditing_table.py index 2c3e2a39..7c6e892f 100644 --- a/app/ch15_deploy/starter/alembic/versions/722c82f0097c_added_auditing_table.py +++ b/app/ch15_deploy/starter/alembic/versions/722c82f0097c_added_auditing_table.py @@ -18,11 +18,12 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('auditing', - sa.Column('id', sa.String(), nullable=False), - sa.Column('created_date', sa.DateTime(), nullable=True), - sa.Column('description', sa.String(), nullable=True), - sa.PrimaryKeyConstraint('id') + op.create_table( + 'auditing', + sa.Column('id', sa.String(), nullable=False), + sa.Column('created_date', sa.DateTime(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id'), ) op.create_index(op.f('ix_auditing_created_date'), 'auditing', ['created_date'], unique=False) # ### end Alembic commands ### diff --git a/app/ch15_deploy/starter/pypi_org/app.py b/app/ch15_deploy/starter/pypi_org/app.py index d1de87c6..ee364816 100644 --- a/app/ch15_deploy/starter/pypi_org/app.py +++ b/app/ch15_deploy/starter/pypi_org/app.py @@ -16,21 +16,18 @@ def main(): def configure(): - print("Configuring Flask app:") + print('Configuring Flask app:') register_blueprints() - print("Registered blueprints") + print('Registered blueprints') setup_db() - print("DB setup completed.") - print("", flush=True) + print('DB setup completed.') + print('', flush=True) def setup_db(): - db_file = os.path.join( - os.path.dirname(__file__), - 'db', - 'pypi.sqlite') + db_file = os.path.join(os.path.dirname(__file__), 'db', 'pypi.sqlite') db_session.global_init(db_file) diff --git a/app/ch15_deploy/starter/pypi_org/bin/basic_inserts.py b/app/ch15_deploy/starter/pypi_org/bin/basic_inserts.py index 3a2f360d..6d04f551 100644 --- a/app/ch15_deploy/starter/pypi_org/bin/basic_inserts.py +++ b/app/ch15_deploy/starter/pypi_org/bin/basic_inserts.py @@ -1,14 +1,13 @@ import os import sys +# Make it run more easily outside of PyCharm +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + import pypi_org.data.db_session as db_session from pypi_org.data.package import Package from pypi_org.data.releases import Release -# Make it run more easily outside of PyCharm -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) - def main(): init_db() @@ -20,24 +19,24 @@ def insert_a_package(): p = Package() p.id = input('Package id / name: ').strip().lower() - p.summary = input("Package summary: ").strip() - p.author_name = input("Author: ").strip() - p.license = input("License: ").strip() + p.summary = input('Package summary: ').strip() + p.author_name = input('Author: ').strip() + p.license = input('License: ').strip() - print("Release 1:") + print('Release 1:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) - print("Release 2:") + print('Release 2:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) session = db_session.create_session() diff --git a/app/ch15_deploy/starter/pypi_org/bin/load_data.py b/app/ch15_deploy/starter/pypi_org/bin/load_data.py index 15237f18..3af7a03d 100644 --- a/app/ch15_deploy/starter/pypi_org/bin/load_data.py +++ b/app/ch15_deploy/starter/pypi_org/bin/load_data.py @@ -7,9 +7,9 @@ import progressbar from dateutil.parser import parse -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) +from pypi_org.infrastructure.num_convert import try_int import pypi_org.data.db_session as db_session from pypi_org.data.languages import ProgrammingLanguage from pypi_org.data.licenses import License @@ -39,7 +39,7 @@ def main(): def do_import_languages(file_data: List[dict]): imported = set() - print("Importing languages ... ", flush=True) + print('Importing languages ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -74,7 +74,7 @@ def do_import_languages(file_data: List[dict]): def do_import_licenses(file_data: List[dict]): imported = set() - print("Importing licenses ... ", flush=True) + print('Importing licenses ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -100,17 +100,17 @@ def do_import_licenses(file_data: List[dict]): def do_summary(): session = db_session.create_session() - print("Final numbers:") - print("Users: {:,}".format(session.query(User).count())) - print("Packages: {:,}".format(session.query(Package).count())) - print("Releases: {:,}".format(session.query(Release).count())) - print("Maintainers: {:,}".format(session.query(Maintainer).count())) - print("Languages: {:,}".format(session.query(ProgrammingLanguage).count())) - print("Licenses: {:,}".format(session.query(License).count())) + print('Final numbers:') + print('Users: {:,}'.format(session.query(User).count())) + print('Packages: {:,}'.format(session.query(Package).count())) + print('Releases: {:,}'.format(session.query(Release).count())) + print('Maintainers: {:,}'.format(session.query(Maintainer).count())) + print('Languages: {:,}'.format(session.query(ProgrammingLanguage).count())) + print('Licenses: {:,}'.format(session.query(License).count())) def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: - print("Importing users ... ", flush=True) + print('Importing users ... ', flush=True) with progressbar.ProgressBar(max_value=len(user_lookup)) as bar: for idx, (email, name) in enumerate(user_lookup.items()): session = db_session.create_session() @@ -134,29 +134,29 @@ def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: def do_import_packages(file_data: List[dict], user_lookup: Dict[str, User]): errored_packages = [] - print("Importing packages and releases ... ", flush=True) + print('Importing packages and releases ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): try: load_package(p, user_lookup) bar.update(idx) except Exception as x: - errored_packages.append((p, " *** Errored out for package {}, {}".format(p.get('package_name'), x))) + errored_packages.append((p, ' *** Errored out for package {}, {}'.format(p.get('package_name'), x))) raise sys.stderr.flush() sys.stdout.flush() print() - print("Completed packages with {} errors.".format(len(errored_packages))) - for (p, txt) in errored_packages: + print('Completed packages with {} errors.'.format(len(errored_packages))) + for p, txt in errored_packages: print(txt) def do_load_files() -> List[dict]: data_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../../data/pypi-top-100')) - print("Loading files from {}".format(data_path)) + print('Loading files from {}'.format(data_path)) files = get_file_names(data_path) - print("Found {:,} files, loading ...".format(len(files)), flush=True) - time.sleep(.1) + print('Found {:,} files, loading ...'.format(len(files)), flush=True) + time.sleep(0.1) file_data = [] with progressbar.ProgressBar(max_value=len(files)) as bar: @@ -171,7 +171,7 @@ def do_load_files() -> List[dict]: def find_users(data: List[dict]) -> dict: - print("Discovering users...", flush=True) + print('Discovering users...', flush=True) found_users = {} with progressbar.ProgressBar(max_value=len(data)) as bar: @@ -184,7 +184,7 @@ def find_users(data: List[dict]) -> dict: sys.stderr.flush() sys.stdout.flush() print() - print("Discovered {:,} users".format(len(found_users))) + print('Discovered {:,} users'.format(len(found_users))) print() return found_users @@ -215,7 +215,7 @@ def load_file_data(filename: str) -> dict: with open(filename, 'r', encoding='utf-8') as fin: data = json.load(fin) except Exception as x: - print("ERROR in file: {}, details: {}".format(filename, x), flush=True) + print('ERROR in file: {}, details: {}'.format(filename, x), flush=True) raise return data @@ -233,7 +233,7 @@ def load_package(data: dict, user_lookup: Dict[str, User]): p.author = info.get('author') p.author_email = info.get('author_email') - releases = build_releases(p.id, data.get("releases", {})) + releases = build_releases(p.id, data.get('releases', {})) if releases: p.created_date = releases[0].created_date @@ -283,18 +283,13 @@ def detect_license(license_text: str) -> Optional[str]: license_text = license_text.strip() if len(license_text) > 100 or '\n' in license_text: - return "CUSTOM" + return 'CUSTOM' - license_text = license_text \ - .replace('Software License', '') \ - .replace('License', '') + license_text = license_text.replace('Software License', '').replace('License', '') if '::' in license_text: # E.g. 'License :: OSI Approved :: Apache Software License' - return license_text \ - .split(':')[-1] \ - .replace(' ', ' ') \ - .strip() + return license_text.split(':')[-1].replace(' ', ' ').strip() return license_text.strip() @@ -339,13 +334,6 @@ def make_version_num(version_text): return major, minor, build -def try_int(text) -> int: - try: - return int(text) - except: - return 0 - - def init_db(): top_folder = os.path.dirname(__file__) rel_file = os.path.join('..', 'db', 'pypi.sqlite') @@ -357,9 +345,7 @@ def get_file_names(data_path: str) -> List[str]: files = [] for f in os.listdir(data_path): if f.endswith('.json'): - files.append( - os.path.abspath(os.path.join(data_path, f)) - ) + files.append(os.path.abspath(os.path.join(data_path, f))) files.sort() return files diff --git a/app/ch15_deploy/starter/pypi_org/data/__all_models.py b/app/ch15_deploy/starter/pypi_org/data/__all_models.py index f399ef54..ca4804ac 100644 --- a/app/ch15_deploy/starter/pypi_org/data/__all_models.py +++ b/app/ch15_deploy/starter/pypi_org/data/__all_models.py @@ -5,17 +5,24 @@ # noinspection PyUnresolvedReferences import pypi_org.data.audit + # noinspection PyUnresolvedReferences import pypi_org.data.downloads + # noinspection PyUnresolvedReferences import pypi_org.data.languages + # noinspection PyUnresolvedReferences import pypi_org.data.licenses + # noinspection PyUnresolvedReferences import pypi_org.data.maintainers + # noinspection PyUnresolvedReferences import pypi_org.data.package + # noinspection PyUnresolvedReferences import pypi_org.data.releases + # noinspection PyUnresolvedReferences import pypi_org.data.users diff --git a/app/ch15_deploy/starter/pypi_org/data/db_session.py b/app/ch15_deploy/starter/pypi_org/data/db_session.py index acfc485d..86eb3fb2 100644 --- a/app/ch15_deploy/starter/pypi_org/data/db_session.py +++ b/app/ch15_deploy/starter/pypi_org/data/db_session.py @@ -14,12 +14,15 @@ def global_init(db_file: str): return if not db_file or not db_file.strip(): - raise Exception("You must specify a db file.") + raise Exception('You must specify a db file.') conn_str = 'sqlite:///' + db_file.strip() - print("Connecting to DB with {}".format(conn_str)) + print('Connecting to DB with {}'.format(conn_str)) - engine = sa.create_engine(conn_str, echo=False) + # Adding check_same_thread = False after the recording. This can be an issue about + # creating / owner thread when cleaning up sessions, etc. This is a sqlite restriction + # that we probably don't care about in this example. + engine = sa.create_engine(conn_str, echo=False, connect_args={'check_same_thread': False}) __factory = orm.sessionmaker(bind=engine) # noinspection PyUnresolvedReferences diff --git a/app/ch15_deploy/starter/pypi_org/data/downloads.py b/app/ch15_deploy/starter/pypi_org/data/downloads.py index 66068f02..dd8afd65 100644 --- a/app/ch15_deploy/starter/pypi_org/data/downloads.py +++ b/app/ch15_deploy/starter/pypi_org/data/downloads.py @@ -7,8 +7,7 @@ class Download(SqlAlchemyBase): __tablename__ = 'downloads' id: int = sqlalchemy.Column(sqlalchemy.BigInteger, primary_key=True, autoincrement=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) package_id: str = sqlalchemy.Column(sqlalchemy.String, index=True) release_id: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) diff --git a/app/ch15_deploy/starter/pypi_org/data/languages.py b/app/ch15_deploy/starter/pypi_org/data/languages.py index 964b8444..3a73dde0 100644 --- a/app/ch15_deploy/starter/pypi_org/data/languages.py +++ b/app/ch15_deploy/starter/pypi_org/data/languages.py @@ -7,6 +7,5 @@ class ProgrammingLanguage(SqlAlchemyBase): __tablename__ = 'languages' id: str = sqlalchemy.Column(sqlalchemy.String, primary_key=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) description: str = sqlalchemy.Column(sqlalchemy.String) diff --git a/app/ch15_deploy/starter/pypi_org/data/package.py b/app/ch15_deploy/starter/pypi_org/data/package.py index c3420d1b..e0e48878 100644 --- a/app/ch15_deploy/starter/pypi_org/data/package.py +++ b/app/ch15_deploy/starter/pypi_org/data/package.py @@ -3,6 +3,8 @@ import sqlalchemy as sa import sqlalchemy.orm as orm +from sqlalchemy.orm import Mapped + from pypi_org.data.modelbase import SqlAlchemyBase from pypi_org.data.releases import Release @@ -12,7 +14,6 @@ class Package(SqlAlchemyBase): id: str = sa.Column(sa.String, primary_key=True) created_date: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) - last_updated: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) summary: str = sa.Column(sa.String, nullable=False) description: str = sa.Column(sa.String, nullable=True) @@ -26,11 +27,15 @@ class Package(SqlAlchemyBase): license: str = sa.Column(sa.String, index=True) # releases relationship - releases: List[Release] = orm.relation("Release", order_by=[ - Release.major_ver.desc(), - Release.minor_ver.desc(), - Release.build_ver.desc(), - ], back_populates='package') + releases: Mapped[Release] = orm.relationship( + 'Release', + order_by=[ + Release.major_ver.desc(), + Release.minor_ver.desc(), + Release.build_ver.desc(), + ], + back_populates='package', + ) def __repr__(self): return ''.format(self.id) diff --git a/app/ch15_deploy/starter/pypi_org/data/releases.py b/app/ch15_deploy/starter/pypi_org/data/releases.py index 7650e438..4b2db2bf 100644 --- a/app/ch15_deploy/starter/pypi_org/data/releases.py +++ b/app/ch15_deploy/starter/pypi_org/data/releases.py @@ -13,16 +13,14 @@ class Release(SqlAlchemyBase): minor_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) build_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) - created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, - default=datetime.datetime.now, - index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) comment: str = sqlalchemy.Column(sqlalchemy.String) url: str = sqlalchemy.Column(sqlalchemy.String) size: int = sqlalchemy.Column(sqlalchemy.BigInteger) # Package relationship - package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey("packages.id")) - package = orm.relation('Package') + package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey('packages.id')) + package = orm.relationship('Package') @property def version_text(self): diff --git a/app/ch15_deploy/starter/pypi_org/infrastructure/cookie_auth.py b/app/ch15_deploy/starter/pypi_org/infrastructure/cookie_auth.py index 64dc85a7..c215c0af 100644 --- a/app/ch15_deploy/starter/pypi_org/infrastructure/cookie_auth.py +++ b/app/ch15_deploy/starter/pypi_org/infrastructure/cookie_auth.py @@ -1,19 +1,18 @@ import hashlib -from datetime import timedelta from typing import Optional from flask import Request from flask import Response -from pypi_org.bin.load_data import try_int +from pypi_org.infrastructure.num_convert import try_int auth_cookie_name = 'pypi_demo_user' def set_auth(response: Response, user_id: int): hash_val = __hash_text(str(user_id)) - val = "{}:{}".format(user_id, hash_val) - response.set_cookie(auth_cookie_name, val) + val = '{}:{}'.format(user_id, hash_val) + response.set_cookie(auth_cookie_name, val, secure=False, httponly=True, samesite='Lax') def __hash_text(text: str) -> str: @@ -21,10 +20,6 @@ def __hash_text(text: str) -> str: return hashlib.sha512(text.encode('utf-8')).hexdigest() -def __add_cookie_callback(_, response: Response, name: str, value: str): - response.set_cookie(name, value, max_age=timedelta(days=30)) - - def get_user_id_via_auth_cookie(request: Request) -> Optional[int]: if auth_cookie_name not in request.cookies: return None @@ -38,7 +33,7 @@ def get_user_id_via_auth_cookie(request: Request) -> Optional[int]: hash_val = parts[1] hash_val_check = __hash_text(user_id) if hash_val != hash_val_check: - print("Warning: Hash mismatch, invalid cookie value") + print('Warning: Hash mismatch, invalid cookie value') return None return try_int(user_id) diff --git a/app/ch15_deploy/starter/pypi_org/infrastructure/num_convert.py b/app/ch15_deploy/starter/pypi_org/infrastructure/num_convert.py new file mode 100644 index 00000000..4e1d8879 --- /dev/null +++ b/app/ch15_deploy/starter/pypi_org/infrastructure/num_convert.py @@ -0,0 +1,8 @@ +from typing import Optional + + +def try_int(text) -> Optional[int]: + try: + return int(text) + except: + return None diff --git a/app/ch15_deploy/starter/pypi_org/infrastructure/request_dict.py b/app/ch15_deploy/starter/pypi_org/infrastructure/request_dict.py index 495fd979..d44ae574 100644 --- a/app/ch15_deploy/starter/pypi_org/infrastructure/request_dict.py +++ b/app/ch15_deploy/starter/pypi_org/infrastructure/request_dict.py @@ -1,4 +1,5 @@ import flask +from werkzeug.datastructures import MultiDict class RequestDictionary(dict): @@ -13,11 +14,22 @@ def __getattr__(self, key): def create(default_val=None, **route_args) -> RequestDictionary: request = flask.request + # Adding this retro actively. Some folks are experiencing issues where they + # are getting a list rather than plain dict. I think it's from multiple + # entries in the multidict. This should fix it. + args = request.args + if isinstance(request.args, MultiDict): + args = request.args.to_dict() + + form = request.form + if isinstance(request.args, MultiDict): + form = request.form.to_dict() + data = { - **request.args, # The key/value pairs in the URL query string + **args, # The key/value pairs in the URL query string **request.headers, # Header values - **request.form, # The key/value pairs in the body, from a HTML post form - **route_args # And additional arguments the method access, if they want them merged. + **form, # The key/value pairs in the body, from a HTML post form + **route_args, # And additional arguments the method access, if they want them merged. } return RequestDictionary(data, default_val=default_val) diff --git a/app/ch15_deploy/starter/pypi_org/infrastructure/view_modifiers.py b/app/ch15_deploy/starter/pypi_org/infrastructure/view_modifiers.py index 82ce5baf..d944c142 100644 --- a/app/ch15_deploy/starter/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch15_deploy/starter/pypi_org/infrastructure/view_modifiers.py @@ -1,7 +1,8 @@ from functools import wraps import flask -import werkzeug.wrappers.response +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -11,9 +12,11 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) - if isinstance(response_val, flask.Response): + + if isinstance(response_val, werkzeug.wrappers.Response): return response_val - if isinstance(response_val, werkzeug.wrappers.response.Response): + + if isinstance(response_val, flask.Response): return response_val if isinstance(response_val, dict): @@ -23,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -39,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch15_deploy/starter/pypi_org/services/package_service.py b/app/ch15_deploy/starter/pypi_org/services/package_service.py index b03408d2..9a153773 100644 --- a/app/ch15_deploy/starter/pypi_org/services/package_service.py +++ b/app/ch15_deploy/starter/pypi_org/services/package_service.py @@ -10,12 +10,13 @@ def get_latest_releases(limit=10) -> List[Release]: session = db_session.create_session() try: - - releases = session.query(Release). \ - options(sqlalchemy.orm.joinedload(Release.package)). \ - order_by(Release.created_date.desc()). \ - limit(limit). \ - all() + releases = ( + session.query(Release) + .options(sqlalchemy.orm.joinedload(Release.package)) + .order_by(Release.created_date.desc()) + .limit(limit) + .all() + ) finally: session.close() @@ -47,11 +48,12 @@ def get_package_by_id(package_id: str) -> Optional[Package]: session = db_session.create_session() try: - - package = session.query(Package) \ - .options(sqlalchemy.orm.joinedload(Package.releases)) \ - .filter(Package.id == package_id) \ + package = ( + session.query(Package) + .options(sqlalchemy.orm.joinedload(Package.releases)) + .filter(Package.id == package_id) .first() + ) finally: session.close() diff --git a/app/ch15_deploy/starter/pypi_org/viewmodels/cms/page_viewmodel.py b/app/ch15_deploy/starter/pypi_org/viewmodels/cms/page_viewmodel.py index b7bb3249..ec033c3f 100644 --- a/app/ch15_deploy/starter/pypi_org/viewmodels/cms/page_viewmodel.py +++ b/app/ch15_deploy/starter/pypi_org/viewmodels/cms/page_viewmodel.py @@ -7,4 +7,3 @@ def __init__(self, full_url: str): super().__init__() self.page = cms_service.get_page(full_url) - diff --git a/app/ch15_deploy/starter/pypi_org/viewmodels/packages/pagedetails_viewmodel.py b/app/ch15_deploy/starter/pypi_org/viewmodels/packages/pagedetails_viewmodel.py index 63c4cf68..62bdca3d 100644 --- a/app/ch15_deploy/starter/pypi_org/viewmodels/packages/pagedetails_viewmodel.py +++ b/app/ch15_deploy/starter/pypi_org/viewmodels/packages/pagedetails_viewmodel.py @@ -11,12 +11,12 @@ def __init__(self, package_name: str): self.package_name = package_name.strip().lower() self.package = package_service.get_package_by_id(self.package_name) - self.latest_version = "0.0.0" + self.latest_version = '0.0.0' self.latest_release = None self.is_latest = True if self.package and self.package.releases: - self.latest_release = self.package.releases[0] + self.latest_release = self.package.releases self.latest_version = self.latest_release.version_text self.release_version = self.latest_release diff --git a/app/ch15_deploy/starter/pypi_org/viewmodels/seo/sitemap_viewmodel.py b/app/ch15_deploy/starter/pypi_org/viewmodels/seo/sitemap_viewmodel.py index 912c4df0..71df2f99 100644 --- a/app/ch15_deploy/starter/pypi_org/viewmodels/seo/sitemap_viewmodel.py +++ b/app/ch15_deploy/starter/pypi_org/viewmodels/seo/sitemap_viewmodel.py @@ -8,5 +8,5 @@ class SiteMapViewModel(ViewModelBase): def __init__(self, limit: int): super().__init__() self.packages = package_service.all_packages(limit) - self.last_updated_text = "2019-07-15" - self.site = "{}://{}".format(flask.request.scheme, flask.request.host) + self.last_updated_text = '2019-07-15' + self.site = '{}://{}'.format(flask.request.scheme, flask.request.host) diff --git a/app/ch15_deploy/starter/pypi_org/views/account_views.py b/app/ch15_deploy/starter/pypi_org/views/account_views.py index 8697cc24..652f43b3 100644 --- a/app/ch15_deploy/starter/pypi_org/views/account_views.py +++ b/app/ch15_deploy/starter/pypi_org/views/account_views.py @@ -25,6 +25,7 @@ def index(): # ################### REGISTER ################################# + @blueprint.route('/account/register', methods=['GET']) @response(template_file='account/register.html') def register_get(): @@ -54,10 +55,16 @@ def register_post(): # ################### LOGIN ################################# + @blueprint.route('/account/login', methods=['GET']) @response(template_file='account/login.html') def login_get(): vm = LoginViewModel() + + # Added after recording, see https://github.com/talkpython/data-driven-web-apps-with-flask/issues/24 + if vm.user_id: + return flask.redirect('/account') + return vm.to_dict() @@ -72,7 +79,7 @@ def login_post(): user = user_service.login_user(vm.email, vm.password) if not user: - vm.error = "The account does not exist or the password is wrong." + vm.error = 'The account does not exist or the password is wrong.' return vm.to_dict() resp = flask.redirect('/account') @@ -83,6 +90,7 @@ def login_post(): # ################### LOGOUT ################################# + @blueprint.route('/account/logout') def logout(): resp = flask.redirect('/') diff --git a/app/ch15_deploy/starter/pypi_org/views/package_views.py b/app/ch15_deploy/starter/pypi_org/views/package_views.py index 1a74549f..2f862740 100644 --- a/app/ch15_deploy/starter/pypi_org/views/package_views.py +++ b/app/ch15_deploy/starter/pypi_org/views/package_views.py @@ -19,4 +19,4 @@ def package_details(package_name: str): @blueprint.route('/') def popular(rank: int): print(type(rank), rank) - return "The details for the {}th most popular package".format(rank) + return 'The details for the {}th most popular package'.format(rank) diff --git a/app/ch15_deploy/starter/pypi_org/views/seo_view.py b/app/ch15_deploy/starter/pypi_org/views/seo_view.py index 93c88de5..f86b6bff 100644 --- a/app/ch15_deploy/starter/pypi_org/views/seo_view.py +++ b/app/ch15_deploy/starter/pypi_org/views/seo_view.py @@ -18,6 +18,7 @@ def sitemap(): # ################### Robots ################################# + @blueprint.route('/robots.txt') @response(mimetype='text/plain', template_file='seo/robots.txt') def robots(): diff --git a/app/ch15_deploy/starter/requirements.piptools b/app/ch15_deploy/starter/requirements.piptools new file mode 100644 index 00000000..d13bf0c8 --- /dev/null +++ b/app/ch15_deploy/starter/requirements.piptools @@ -0,0 +1,6 @@ +alembic +flask +passlib +progressbar2 +python-dateutil +sqlalchemy diff --git a/app/ch15_deploy/starter/requirements.txt b/app/ch15_deploy/starter/requirements.txt index e10973d8..19fc8c45 100644 --- a/app/ch15_deploy/starter/requirements.txt +++ b/app/ch15_deploy/starter/requirements.txt @@ -1,8 +1,42 @@ -werkzeug -flask -sqlalchemy - -progressbar2 -python-dateutil -passlib - +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +alembic==1.14.1 + # via -r requirements.piptools +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +mako==1.3.9 + # via alembic +markupsafe==3.0.2 + # via + # jinja2 + # mako + # werkzeug +passlib==1.7.4 + # via -r requirements.piptools +progressbar2==4.5.0 + # via -r requirements.piptools +python-dateutil==2.9.0.post0 + # via -r requirements.piptools +python-utils==3.9.1 + # via progressbar2 +six==1.17.0 + # via python-dateutil +sqlalchemy==2.0.38 + # via + # -r requirements.piptools + # alembic +typing-extensions==4.12.2 + # via + # alembic + # python-utils + # sqlalchemy +werkzeug==3.1.3 + # via flask diff --git a/app/ch15_deploy/starter/tests/_all_tests.py b/app/ch15_deploy/starter/tests/_all_tests.py index daa69ea4..e5c9b9ea 100644 --- a/app/ch15_deploy/starter/tests/_all_tests.py +++ b/app/ch15_deploy/starter/tests/_all_tests.py @@ -1,17 +1,18 @@ import sys import os -container_folder = os.path.abspath(os.path.join( - os.path.dirname(__file__), '..' -)) +container_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, container_folder) # noinspection PyUnresolvedReferences from account_tests import * + # noinspection PyUnresolvedReferences from package_tests import * + # noinspection PyUnresolvedReferences from sitemap_tests import * + # noinspection PyUnresolvedReferences from home_tests import * diff --git a/app/ch15_deploy/starter/tests/account_tests.py b/app/ch15_deploy/starter/tests/account_tests.py index 4f5aa156..e31e8c28 100644 --- a/app/ch15_deploy/starter/tests/account_tests.py +++ b/app/ch15_deploy/starter/tests/account_tests.py @@ -7,7 +7,7 @@ def test_example(): - print("Test example...") + print('Test example...') assert 1 + 2 == 3 @@ -15,11 +15,7 @@ def test_vm_register_validation_when_valid(): # 3 A's of test: Arrange, Act, then Assert # Arrange - form_data = { - 'name': 'Michael', - 'email': 'michael@talkpython.fm', - 'password': 'a' * 6 - } + form_data = {'name': 'Michael', 'email': 'michael@talkpython.fm', 'password': 'a' * 6} with flask_app.test_request_context(path='/account/register', data=form_data): vm = RegisterViewModel() @@ -37,11 +33,7 @@ def test_vm_register_validation_for_existing_user(): # 3 A's of test: Arrange, Act, then Assert # Arrange - form_data = { - 'name': 'Michael', - 'email': 'michael@talkpython.fm', - 'password': 'a' * 6 - } + form_data = {'name': 'Michael', 'email': 'michael@talkpython.fm', 'password': 'a' * 6} with flask_app.test_request_context(path='/account/register', data=form_data): vm = RegisterViewModel() @@ -62,11 +54,8 @@ def test_v_register_view_new_user(): # Arrange from pypi_org.views.account_views import register_post - form_data = { - 'name': 'Michael', - 'email': 'michael@talkpython.fm', - 'password': 'a' * 6 - } + + form_data = {'name': 'Michael', 'email': 'michael@talkpython.fm', 'password': 'a' * 6} target = 'pypi_org.services.user_service.find_user_by_email' find_user = unittest.mock.patch(target, return_value=None) diff --git a/app/ch15_deploy/starter/tests/package_tests.py b/app/ch15_deploy/starter/tests/package_tests.py index efd04ef5..a7c221a5 100644 --- a/app/ch15_deploy/starter/tests/package_tests.py +++ b/app/ch15_deploy/starter/tests/package_tests.py @@ -12,15 +12,14 @@ def test_package_details_success(): test_package = Package() test_package.id = 'sqlalchemy' - test_package.description = "TDB" + test_package.description = 'TDB' test_package.releases = [ Release(created_date=datetime.datetime.now(), major_ver=1, minor_ver=2, build_ver=200), Release(created_date=datetime.datetime.now() - datetime.timedelta(days=10)), ] # Act - with unittest.mock.patch('pypi_org.services.package_service.get_package_by_id', - return_value=test_package): + with unittest.mock.patch('pypi_org.services.package_service.get_package_by_id', return_value=test_package): with flask_app.test_request_context(path='/project/' + test_package.id): resp: Response = package_details(test_package.id) @@ -33,8 +32,7 @@ def test_package_details_404(client): bad_package_url = 'sqlalchemy_missing' # Act - with unittest.mock.patch('pypi_org.services.package_service.get_package_by_id', - return_value=None): + with unittest.mock.patch('pypi_org.services.package_service.get_package_by_id', return_value=None): resp: Response = client.get(bad_package_url) assert resp.status_code == 404 diff --git a/app/ch15_deploy/starter/tests/sitemap_tests.py b/app/ch15_deploy/starter/tests/sitemap_tests.py index b1afe0fe..4494c30a 100644 --- a/app/ch15_deploy/starter/tests/sitemap_tests.py +++ b/app/ch15_deploy/starter/tests/sitemap_tests.py @@ -10,10 +10,7 @@ def test_int_site_mapped_urls(client): href.text.strip().replace('http://127.0.0.1:5000', '').replace('http://localhost', '') for href in list(x.findall('url/loc')) ] - urls = [ - u if u else '/' - for u in urls - ] + urls = [u if u else '/' for u in urls] print('Testing {} urls from sitemap...'.format(len(urls)), flush=True) has_tested_projects = False @@ -40,7 +37,7 @@ def get_sitemap_text(client): # # ... # - res: Response = client.get("/sitemap.xml") - text = res.data.decode("utf-8") + res: Response = client.get('/sitemap.xml') + text = res.data.decode('utf-8') text = text.replace('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"', '') return text diff --git a/app/ch15_deploy/starter/tests/test_client.py b/app/ch15_deploy/starter/tests/test_client.py index ffbea486..4390409a 100644 --- a/app/ch15_deploy/starter/tests/test_client.py +++ b/app/ch15_deploy/starter/tests/test_client.py @@ -4,9 +4,7 @@ import sys import os -container_folder = os.path.abspath(os.path.join( - os.path.dirname(__file__), '..' -)) +container_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, container_folder) import pypi_org.app diff --git a/app/ch16_mongodb/final/alembic/README b/app/ch16_mongodb/final/alembic/README deleted file mode 100644 index 98e4f9c4..00000000 --- a/app/ch16_mongodb/final/alembic/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/app/ch16_mongodb/final/alembic/alembic_helpers.py b/app/ch16_mongodb/final/alembic/alembic_helpers.py deleted file mode 100644 index 561f7a70..00000000 --- a/app/ch16_mongodb/final/alembic/alembic_helpers.py +++ /dev/null @@ -1,16 +0,0 @@ -from alembic import op -from sqlalchemy import engine_from_config -from sqlalchemy.engine import reflection - - -def table_has_column(table, column): - config = op.get_context().config - engine = engine_from_config( - config.get_section(config.config_ini_section), prefix='sqlalchemy.') - insp = reflection.Inspector.from_engine(engine) - has_column = False - for col in insp.get_columns(table): - if column not in col['name']: - continue - has_column = True - return has_column diff --git a/app/ch16_mongodb/final/alembic/env.py b/app/ch16_mongodb/final/alembic/env.py deleted file mode 100644 index 27ec49fb..00000000 --- a/app/ch16_mongodb/final/alembic/env.py +++ /dev/null @@ -1,86 +0,0 @@ -import os -from logging.config import fileConfig - -from sqlalchemy import engine_from_config -from sqlalchemy import pool - -from alembic import context - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. - -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -fileConfig(config.config_file_name) - -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata - -import sys - -folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.insert(0, folder) - -from pypi_org.data.modelbase import SqlAlchemyBase -# noinspection PyUnresolvedReferences -import pypi_org.data.__all_models - -target_metadata = SqlAlchemyBase.metadata - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - - -def run_migrations_offline(): - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, target_metadata=target_metadata, literal_binds=True - ) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online(): - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - connectable = engine_from_config( - config.get_section(config.config_ini_section), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) - - with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/app/ch16_mongodb/final/alembic/script.py.mako b/app/ch16_mongodb/final/alembic/script.py.mako deleted file mode 100644 index 2c015630..00000000 --- a/app/ch16_mongodb/final/alembic/script.py.mako +++ /dev/null @@ -1,24 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} -branch_labels = ${repr(branch_labels)} -depends_on = ${repr(depends_on)} - - -def upgrade(): - ${upgrades if upgrades else "pass"} - - -def downgrade(): - ${downgrades if downgrades else "pass"} diff --git a/app/ch16_mongodb/final/alembic/versions/722c82f0097c_added_auditing_table.py b/app/ch16_mongodb/final/alembic/versions/722c82f0097c_added_auditing_table.py deleted file mode 100644 index 2c3e2a39..00000000 --- a/app/ch16_mongodb/final/alembic/versions/722c82f0097c_added_auditing_table.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Added Auditing table - -Revision ID: 722c82f0097c -Revises: a55036d4e943 -Create Date: 2019-05-16 11:16:42.514539 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '722c82f0097c' -down_revision = 'a55036d4e943' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('auditing', - sa.Column('id', sa.String(), nullable=False), - sa.Column('created_date', sa.DateTime(), nullable=True), - sa.Column('description', sa.String(), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_auditing_created_date'), 'auditing', ['created_date'], unique=False) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_auditing_created_date'), table_name='auditing') - op.drop_table('auditing') - # ### end Alembic commands ### diff --git a/app/ch16_mongodb/final/alembic/versions/a55036d4e943_added_last_updated.py b/app/ch16_mongodb/final/alembic/versions/a55036d4e943_added_last_updated.py deleted file mode 100644 index 074ce04a..00000000 --- a/app/ch16_mongodb/final/alembic/versions/a55036d4e943_added_last_updated.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Added last updated - -Revision ID: a55036d4e943 -Revises: -Create Date: 2019-05-16 10:55:28.413573 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'a55036d4e943' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('packages', sa.Column('last_updated', sa.DateTime(), nullable=True)) - op.create_index(op.f('ix_packages_last_updated'), 'packages', ['last_updated'], unique=False) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_packages_last_updated'), table_name='packages') - op.drop_column('packages', 'last_updated') - # ### end Alembic commands ### diff --git a/app/ch16_mongodb/final/pypi_org/app.py b/app/ch16_mongodb/final/pypi_org/app.py index 38ba85d7..2b98b408 100644 --- a/app/ch16_mongodb/final/pypi_org/app.py +++ b/app/ch16_mongodb/final/pypi_org/app.py @@ -19,14 +19,14 @@ def main(): def configure(): - print("Configuring Flask app:") + print('Configuring Flask app:') register_blueprints() - print("Registered blueprints") + print('Registered blueprints') setup_db() - print("DB setup completed.") - print("", flush=True) + print('DB setup completed.') + print('', flush=True) def setup_db(): diff --git a/app/ch16_mongodb/final/pypi_org/bin/basic_inserts.py b/app/ch16_mongodb/final/pypi_org/bin/basic_inserts.py index 3a2f360d..6d04f551 100644 --- a/app/ch16_mongodb/final/pypi_org/bin/basic_inserts.py +++ b/app/ch16_mongodb/final/pypi_org/bin/basic_inserts.py @@ -1,14 +1,13 @@ import os import sys +# Make it run more easily outside of PyCharm +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + import pypi_org.data.db_session as db_session from pypi_org.data.package import Package from pypi_org.data.releases import Release -# Make it run more easily outside of PyCharm -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) - def main(): init_db() @@ -20,24 +19,24 @@ def insert_a_package(): p = Package() p.id = input('Package id / name: ').strip().lower() - p.summary = input("Package summary: ").strip() - p.author_name = input("Author: ").strip() - p.license = input("License: ").strip() + p.summary = input('Package summary: ').strip() + p.author_name = input('Author: ').strip() + p.license = input('License: ').strip() - print("Release 1:") + print('Release 1:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) - print("Release 2:") + print('Release 2:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) session = db_session.create_session() diff --git a/app/ch16_mongodb/final/pypi_org/bin/load_data.py b/app/ch16_mongodb/final/pypi_org/bin/load_data.py index 15237f18..228c1b6b 100644 --- a/app/ch16_mongodb/final/pypi_org/bin/load_data.py +++ b/app/ch16_mongodb/final/pypi_org/bin/load_data.py @@ -7,8 +7,7 @@ import progressbar from dateutil.parser import parse -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) import pypi_org.data.db_session as db_session from pypi_org.data.languages import ProgrammingLanguage @@ -39,7 +38,7 @@ def main(): def do_import_languages(file_data: List[dict]): imported = set() - print("Importing languages ... ", flush=True) + print('Importing languages ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -74,7 +73,7 @@ def do_import_languages(file_data: List[dict]): def do_import_licenses(file_data: List[dict]): imported = set() - print("Importing licenses ... ", flush=True) + print('Importing licenses ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -100,17 +99,17 @@ def do_import_licenses(file_data: List[dict]): def do_summary(): session = db_session.create_session() - print("Final numbers:") - print("Users: {:,}".format(session.query(User).count())) - print("Packages: {:,}".format(session.query(Package).count())) - print("Releases: {:,}".format(session.query(Release).count())) - print("Maintainers: {:,}".format(session.query(Maintainer).count())) - print("Languages: {:,}".format(session.query(ProgrammingLanguage).count())) - print("Licenses: {:,}".format(session.query(License).count())) + print('Final numbers:') + print('Users: {:,}'.format(session.query(User).count())) + print('Packages: {:,}'.format(session.query(Package).count())) + print('Releases: {:,}'.format(session.query(Release).count())) + print('Maintainers: {:,}'.format(session.query(Maintainer).count())) + print('Languages: {:,}'.format(session.query(ProgrammingLanguage).count())) + print('Licenses: {:,}'.format(session.query(License).count())) def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: - print("Importing users ... ", flush=True) + print('Importing users ... ', flush=True) with progressbar.ProgressBar(max_value=len(user_lookup)) as bar: for idx, (email, name) in enumerate(user_lookup.items()): session = db_session.create_session() @@ -134,29 +133,29 @@ def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: def do_import_packages(file_data: List[dict], user_lookup: Dict[str, User]): errored_packages = [] - print("Importing packages and releases ... ", flush=True) + print('Importing packages and releases ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): try: load_package(p, user_lookup) bar.update(idx) except Exception as x: - errored_packages.append((p, " *** Errored out for package {}, {}".format(p.get('package_name'), x))) + errored_packages.append((p, ' *** Errored out for package {}, {}'.format(p.get('package_name'), x))) raise sys.stderr.flush() sys.stdout.flush() print() - print("Completed packages with {} errors.".format(len(errored_packages))) - for (p, txt) in errored_packages: + print('Completed packages with {} errors.'.format(len(errored_packages))) + for p, txt in errored_packages: print(txt) def do_load_files() -> List[dict]: data_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../../data/pypi-top-100')) - print("Loading files from {}".format(data_path)) + print('Loading files from {}'.format(data_path)) files = get_file_names(data_path) - print("Found {:,} files, loading ...".format(len(files)), flush=True) - time.sleep(.1) + print('Found {:,} files, loading ...'.format(len(files)), flush=True) + time.sleep(0.1) file_data = [] with progressbar.ProgressBar(max_value=len(files)) as bar: @@ -171,7 +170,7 @@ def do_load_files() -> List[dict]: def find_users(data: List[dict]) -> dict: - print("Discovering users...", flush=True) + print('Discovering users...', flush=True) found_users = {} with progressbar.ProgressBar(max_value=len(data)) as bar: @@ -184,7 +183,7 @@ def find_users(data: List[dict]) -> dict: sys.stderr.flush() sys.stdout.flush() print() - print("Discovered {:,} users".format(len(found_users))) + print('Discovered {:,} users'.format(len(found_users))) print() return found_users @@ -215,7 +214,7 @@ def load_file_data(filename: str) -> dict: with open(filename, 'r', encoding='utf-8') as fin: data = json.load(fin) except Exception as x: - print("ERROR in file: {}, details: {}".format(filename, x), flush=True) + print('ERROR in file: {}, details: {}'.format(filename, x), flush=True) raise return data @@ -233,7 +232,7 @@ def load_package(data: dict, user_lookup: Dict[str, User]): p.author = info.get('author') p.author_email = info.get('author_email') - releases = build_releases(p.id, data.get("releases", {})) + releases = build_releases(p.id, data.get('releases', {})) if releases: p.created_date = releases[0].created_date @@ -283,18 +282,13 @@ def detect_license(license_text: str) -> Optional[str]: license_text = license_text.strip() if len(license_text) > 100 or '\n' in license_text: - return "CUSTOM" + return 'CUSTOM' - license_text = license_text \ - .replace('Software License', '') \ - .replace('License', '') + license_text = license_text.replace('Software License', '').replace('License', '') if '::' in license_text: # E.g. 'License :: OSI Approved :: Apache Software License' - return license_text \ - .split(':')[-1] \ - .replace(' ', ' ') \ - .strip() + return license_text.split(':')[-1].replace(' ', ' ').strip() return license_text.strip() @@ -339,11 +333,11 @@ def make_version_num(version_text): return major, minor, build -def try_int(text) -> int: +def try_int(text) -> Optional[int]: try: return int(text) except: - return 0 + return None def init_db(): @@ -357,9 +351,7 @@ def get_file_names(data_path: str) -> List[str]: files = [] for f in os.listdir(data_path): if f.endswith('.json'): - files.append( - os.path.abspath(os.path.join(data_path, f)) - ) + files.append(os.path.abspath(os.path.join(data_path, f))) files.sort() return files diff --git a/app/ch16_mongodb/final/pypi_org/bin/migrate_to_mongodb.py b/app/ch16_mongodb/final/pypi_org/bin/migrate_to_mongodb.py index f56a1e32..51a6c105 100644 --- a/app/ch16_mongodb/final/pypi_org/bin/migrate_to_mongodb.py +++ b/app/ch16_mongodb/final/pypi_org/bin/migrate_to_mongodb.py @@ -84,13 +84,7 @@ def migrate_releases(): def init_dbs(): - db_file = os.path.abspath( - os.path.join( - os.path.dirname(__file__), - '..', - 'db', - 'pypi.sqlite' - )) + db_file = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'db', 'pypi.sqlite')) db_session.global_init(db_file) mongo_setup.global_init() diff --git a/app/ch16_mongodb/final/pypi_org/data/__all_models.py b/app/ch16_mongodb/final/pypi_org/data/__all_models.py index f399ef54..ca4804ac 100644 --- a/app/ch16_mongodb/final/pypi_org/data/__all_models.py +++ b/app/ch16_mongodb/final/pypi_org/data/__all_models.py @@ -5,17 +5,24 @@ # noinspection PyUnresolvedReferences import pypi_org.data.audit + # noinspection PyUnresolvedReferences import pypi_org.data.downloads + # noinspection PyUnresolvedReferences import pypi_org.data.languages + # noinspection PyUnresolvedReferences import pypi_org.data.licenses + # noinspection PyUnresolvedReferences import pypi_org.data.maintainers + # noinspection PyUnresolvedReferences import pypi_org.data.package + # noinspection PyUnresolvedReferences import pypi_org.data.releases + # noinspection PyUnresolvedReferences import pypi_org.data.users diff --git a/app/ch16_mongodb/final/pypi_org/data/db_session.py b/app/ch16_mongodb/final/pypi_org/data/db_session.py index acfc485d..a1c4df68 100644 --- a/app/ch16_mongodb/final/pypi_org/data/db_session.py +++ b/app/ch16_mongodb/final/pypi_org/data/db_session.py @@ -14,10 +14,10 @@ def global_init(db_file: str): return if not db_file or not db_file.strip(): - raise Exception("You must specify a db file.") + raise Exception('You must specify a db file.') conn_str = 'sqlite:///' + db_file.strip() - print("Connecting to DB with {}".format(conn_str)) + print('Connecting to DB with {}'.format(conn_str)) engine = sa.create_engine(conn_str, echo=False) __factory = orm.sessionmaker(bind=engine) diff --git a/app/ch16_mongodb/final/pypi_org/data/downloads.py b/app/ch16_mongodb/final/pypi_org/data/downloads.py index 66068f02..dd8afd65 100644 --- a/app/ch16_mongodb/final/pypi_org/data/downloads.py +++ b/app/ch16_mongodb/final/pypi_org/data/downloads.py @@ -7,8 +7,7 @@ class Download(SqlAlchemyBase): __tablename__ = 'downloads' id: int = sqlalchemy.Column(sqlalchemy.BigInteger, primary_key=True, autoincrement=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) package_id: str = sqlalchemy.Column(sqlalchemy.String, index=True) release_id: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) diff --git a/app/ch16_mongodb/final/pypi_org/data/languages.py b/app/ch16_mongodb/final/pypi_org/data/languages.py index 964b8444..3a73dde0 100644 --- a/app/ch16_mongodb/final/pypi_org/data/languages.py +++ b/app/ch16_mongodb/final/pypi_org/data/languages.py @@ -7,6 +7,5 @@ class ProgrammingLanguage(SqlAlchemyBase): __tablename__ = 'languages' id: str = sqlalchemy.Column(sqlalchemy.String, primary_key=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) description: str = sqlalchemy.Column(sqlalchemy.String) diff --git a/app/ch16_mongodb/final/pypi_org/data/package.py b/app/ch16_mongodb/final/pypi_org/data/package.py index c3420d1b..e0e48878 100644 --- a/app/ch16_mongodb/final/pypi_org/data/package.py +++ b/app/ch16_mongodb/final/pypi_org/data/package.py @@ -3,6 +3,8 @@ import sqlalchemy as sa import sqlalchemy.orm as orm +from sqlalchemy.orm import Mapped + from pypi_org.data.modelbase import SqlAlchemyBase from pypi_org.data.releases import Release @@ -12,7 +14,6 @@ class Package(SqlAlchemyBase): id: str = sa.Column(sa.String, primary_key=True) created_date: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) - last_updated: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) summary: str = sa.Column(sa.String, nullable=False) description: str = sa.Column(sa.String, nullable=True) @@ -26,11 +27,15 @@ class Package(SqlAlchemyBase): license: str = sa.Column(sa.String, index=True) # releases relationship - releases: List[Release] = orm.relation("Release", order_by=[ - Release.major_ver.desc(), - Release.minor_ver.desc(), - Release.build_ver.desc(), - ], back_populates='package') + releases: Mapped[Release] = orm.relationship( + 'Release', + order_by=[ + Release.major_ver.desc(), + Release.minor_ver.desc(), + Release.build_ver.desc(), + ], + back_populates='package', + ) def __repr__(self): return ''.format(self.id) diff --git a/app/ch16_mongodb/final/pypi_org/data/releases.py b/app/ch16_mongodb/final/pypi_org/data/releases.py index 7650e438..4b2db2bf 100644 --- a/app/ch16_mongodb/final/pypi_org/data/releases.py +++ b/app/ch16_mongodb/final/pypi_org/data/releases.py @@ -13,16 +13,14 @@ class Release(SqlAlchemyBase): minor_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) build_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) - created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, - default=datetime.datetime.now, - index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) comment: str = sqlalchemy.Column(sqlalchemy.String) url: str = sqlalchemy.Column(sqlalchemy.String) size: int = sqlalchemy.Column(sqlalchemy.BigInteger) # Package relationship - package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey("packages.id")) - package = orm.relation('Package') + package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey('packages.id')) + package = orm.relationship('Package') @property def version_text(self): diff --git a/app/ch16_mongodb/final/pypi_org/infrastructure/cookie_auth.py b/app/ch16_mongodb/final/pypi_org/infrastructure/cookie_auth.py index fdbf28b8..aa2f4c2b 100644 --- a/app/ch16_mongodb/final/pypi_org/infrastructure/cookie_auth.py +++ b/app/ch16_mongodb/final/pypi_org/infrastructure/cookie_auth.py @@ -1,20 +1,19 @@ import hashlib -from datetime import timedelta from typing import Optional import bson from flask import Request from flask import Response -from pypi_org.bin.load_data import try_int +from pypi_org.infrastructure.num_convert import try_object_id auth_cookie_name = 'pypi_demo_user' -def set_auth(response: Response, user_id: int): +def set_auth(response: Response, user_id: bson.ObjectId): hash_val = __hash_text(str(user_id)) - val = "{}:{}".format(user_id, hash_val) - response.set_cookie(auth_cookie_name, val) + val = '{}:{}'.format(user_id, hash_val) + response.set_cookie(auth_cookie_name, val, secure=False, httponly=True, samesite='Lax') def __hash_text(text: str) -> str: @@ -22,10 +21,6 @@ def __hash_text(text: str) -> str: return hashlib.sha512(text.encode('utf-8')).hexdigest() -def __add_cookie_callback(_, response: Response, name: str, value: str): - response.set_cookie(name, value, max_age=timedelta(days=30)) - - def get_user_id_via_auth_cookie(request: Request) -> Optional[bson.ObjectId]: if auth_cookie_name not in request.cookies: return None @@ -39,13 +34,10 @@ def get_user_id_via_auth_cookie(request: Request) -> Optional[bson.ObjectId]: hash_val = parts[1] hash_val_check = __hash_text(user_id) if hash_val != hash_val_check: - print("Warning: Hash mismatch, invalid cookie value") + print('Warning: Hash mismatch, invalid cookie value') return None - try: - return bson.ObjectId(user_id) - except: - return None + return try_object_id(user_id) def logout(response: Response): diff --git a/app/ch16_mongodb/final/pypi_org/infrastructure/num_convert.py b/app/ch16_mongodb/final/pypi_org/infrastructure/num_convert.py new file mode 100644 index 00000000..007e3ff8 --- /dev/null +++ b/app/ch16_mongodb/final/pypi_org/infrastructure/num_convert.py @@ -0,0 +1,17 @@ +from typing import Optional + +import bson + + +def try_int(text) -> Optional[int]: + try: + return int(text) + except: + return None + + +def try_object_id(text) -> Optional[bson.ObjectId]: + try: + return bson.ObjectId(text) + except: + return None diff --git a/app/ch16_mongodb/final/pypi_org/infrastructure/request_dict.py b/app/ch16_mongodb/final/pypi_org/infrastructure/request_dict.py index 495fd979..d44ae574 100644 --- a/app/ch16_mongodb/final/pypi_org/infrastructure/request_dict.py +++ b/app/ch16_mongodb/final/pypi_org/infrastructure/request_dict.py @@ -1,4 +1,5 @@ import flask +from werkzeug.datastructures import MultiDict class RequestDictionary(dict): @@ -13,11 +14,22 @@ def __getattr__(self, key): def create(default_val=None, **route_args) -> RequestDictionary: request = flask.request + # Adding this retro actively. Some folks are experiencing issues where they + # are getting a list rather than plain dict. I think it's from multiple + # entries in the multidict. This should fix it. + args = request.args + if isinstance(request.args, MultiDict): + args = request.args.to_dict() + + form = request.form + if isinstance(request.args, MultiDict): + form = request.form.to_dict() + data = { - **request.args, # The key/value pairs in the URL query string + **args, # The key/value pairs in the URL query string **request.headers, # Header values - **request.form, # The key/value pairs in the body, from a HTML post form - **route_args # And additional arguments the method access, if they want them merged. + **form, # The key/value pairs in the body, from a HTML post form + **route_args, # And additional arguments the method access, if they want them merged. } return RequestDictionary(data, default_val=default_val) diff --git a/app/ch16_mongodb/final/pypi_org/infrastructure/view_modifiers.py b/app/ch16_mongodb/final/pypi_org/infrastructure/view_modifiers.py index 82ce5baf..d944c142 100644 --- a/app/ch16_mongodb/final/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch16_mongodb/final/pypi_org/infrastructure/view_modifiers.py @@ -1,7 +1,8 @@ from functools import wraps import flask -import werkzeug.wrappers.response +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -11,9 +12,11 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) - if isinstance(response_val, flask.Response): + + if isinstance(response_val, werkzeug.wrappers.Response): return response_val - if isinstance(response_val, werkzeug.wrappers.response.Response): + + if isinstance(response_val, flask.Response): return response_val if isinstance(response_val, dict): @@ -23,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -39,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch16_mongodb/final/pypi_org/nosql/downloads.py b/app/ch16_mongodb/final/pypi_org/nosql/downloads.py index 6be34547..3cdaa009 100644 --- a/app/ch16_mongodb/final/pypi_org/nosql/downloads.py +++ b/app/ch16_mongodb/final/pypi_org/nosql/downloads.py @@ -18,5 +18,5 @@ class Download(mongoengine.Document): 'created_date', 'package_id', 'release_id', - ] + ], } diff --git a/app/ch16_mongodb/final/pypi_org/nosql/languages.py b/app/ch16_mongodb/final/pypi_org/nosql/languages.py index 5251cb9f..b2042d52 100644 --- a/app/ch16_mongodb/final/pypi_org/nosql/languages.py +++ b/app/ch16_mongodb/final/pypi_org/nosql/languages.py @@ -3,7 +3,6 @@ class ProgrammingLanguage(mongoengine.Document): - id = mongoengine.StringField(primary_key=True) created_date = mongoengine.DateTimeField(default=datetime.datetime.now) description = mongoengine.StringField() @@ -13,5 +12,5 @@ class ProgrammingLanguage(mongoengine.Document): 'collection': 'languages', 'indexes': [ 'created_date', - ] + ], } diff --git a/app/ch16_mongodb/final/pypi_org/nosql/licenses.py b/app/ch16_mongodb/final/pypi_org/nosql/licenses.py index c7190069..daf5a004 100644 --- a/app/ch16_mongodb/final/pypi_org/nosql/licenses.py +++ b/app/ch16_mongodb/final/pypi_org/nosql/licenses.py @@ -12,5 +12,5 @@ class License(mongoengine.Document): 'collection': 'licenses', 'indexes': [ 'created_date', - ] + ], } diff --git a/app/ch16_mongodb/final/pypi_org/nosql/mongo_setup.py b/app/ch16_mongodb/final/pypi_org/nosql/mongo_setup.py index 56694d6a..4fb8a8ae 100644 --- a/app/ch16_mongodb/final/pypi_org/nosql/mongo_setup.py +++ b/app/ch16_mongodb/final/pypi_org/nosql/mongo_setup.py @@ -3,8 +3,7 @@ import mongoengine -def global_init(user=None, password=None, port=27017, - server='localhost', use_ssl=True, db_name='pypi'): +def global_init(user=None, password=None, port=27017, server='localhost', use_ssl=True, db_name='pypi'): if user or password: # noinspection PyUnresolvedReferences data = dict( @@ -15,10 +14,11 @@ def global_init(user=None, password=None, port=27017, authentication_source='admin', authentication_mechanism='SCRAM-SHA-1', ssl=use_ssl, - ssl_cert_reqs=ssl.CERT_NONE) + ssl_cert_reqs=ssl.CERT_NONE, + ) mongoengine.register_connection(alias='core', name=db_name, **data) data['password'] = '*************' - print(" --> Registering prod connection: {}".format(data)) + print(' --> Registering prod connection: {}'.format(data)) else: - print(" --> Registering dev connection") + print(' --> Registering dev connection') mongoengine.register_connection(alias='core', name=db_name) diff --git a/app/ch16_mongodb/final/pypi_org/nosql/packages.py b/app/ch16_mongodb/final/pypi_org/nosql/packages.py index 3f426a03..47f3354b 100644 --- a/app/ch16_mongodb/final/pypi_org/nosql/packages.py +++ b/app/ch16_mongodb/final/pypi_org/nosql/packages.py @@ -26,7 +26,7 @@ class Package(mongoengine.Document): 'created_date', 'author_email', 'license', - ] + ], } def __repr__(self): diff --git a/app/ch16_mongodb/final/pypi_org/nosql/releases.py b/app/ch16_mongodb/final/pypi_org/nosql/releases.py index 4a0623e3..9ad11a67 100644 --- a/app/ch16_mongodb/final/pypi_org/nosql/releases.py +++ b/app/ch16_mongodb/final/pypi_org/nosql/releases.py @@ -26,7 +26,7 @@ class Release(mongoengine.Document): 'build_ver', {'fields': ['major_ver', 'minor_ver', 'build_ver']}, {'fields': ['-major_ver', '-minor_ver', '-build_ver']}, - ] + ], } @property diff --git a/app/ch16_mongodb/final/pypi_org/nosql/users.py b/app/ch16_mongodb/final/pypi_org/nosql/users.py index 96ce7d65..6757e067 100644 --- a/app/ch16_mongodb/final/pypi_org/nosql/users.py +++ b/app/ch16_mongodb/final/pypi_org/nosql/users.py @@ -15,5 +15,5 @@ class User(mongoengine.Document): 'email', 'hashed_password', 'created_date', - ] + ], } diff --git a/app/ch16_mongodb/final/pypi_org/services/package_service.py b/app/ch16_mongodb/final/pypi_org/services/package_service.py index 1d6ea91e..8de9ed80 100644 --- a/app/ch16_mongodb/final/pypi_org/services/package_service.py +++ b/app/ch16_mongodb/final/pypi_org/services/package_service.py @@ -5,10 +5,7 @@ def get_latest_releases(limit=10) -> List[Release]: - releases = Release.objects(). \ - order_by("-created_date"). \ - limit(limit). \ - all() + releases = Release.objects().order_by('-created_date').limit(limit).all() return releases @@ -26,9 +23,7 @@ def get_package_by_id(package_id: str) -> Optional[Package]: package_id = package_id.strip().lower() - package = Package.objects() \ - .filter(id=package_id) \ - .first() + package = Package.objects().filter(id=package_id).first() return package diff --git a/app/ch16_mongodb/final/pypi_org/services/user_service.py b/app/ch16_mongodb/final/pypi_org/services/user_service.py index bdbef70b..edf62641 100644 --- a/app/ch16_mongodb/final/pypi_org/services/user_service.py +++ b/app/ch16_mongodb/final/pypi_org/services/user_service.py @@ -1,5 +1,6 @@ from typing import Optional +import bson from passlib.handlers.sha2_crypt import sha512_crypt as crypto from pypi_org.nosql.users import User @@ -46,6 +47,6 @@ def login_user(email: str, password: str) -> Optional[User]: return user -def find_user_by_id(user_id: int) -> Optional[User]: +def find_user_by_id(user_id: bson.ObjectId) -> Optional[User]: user = User.objects().filter(id=user_id).first() return user diff --git a/app/ch16_mongodb/final/pypi_org/viewmodels/packages/pagedetails_viewmodel.py b/app/ch16_mongodb/final/pypi_org/viewmodels/packages/pagedetails_viewmodel.py index d25f3c70..3a2f6935 100644 --- a/app/ch16_mongodb/final/pypi_org/viewmodels/packages/pagedetails_viewmodel.py +++ b/app/ch16_mongodb/final/pypi_org/viewmodels/packages/pagedetails_viewmodel.py @@ -11,13 +11,12 @@ def __init__(self, package_name: str): self.package_name = package_name.strip().lower() self.package = package_service.get_package_by_id(self.package_name) - self.latest_version = "0.0.0" + self.latest_version = '0.0.0' self.latest_release = None self.is_latest = True if self.package: - self.latest_release = package_service.get_latest_release_for_package( - self.package.id) + self.latest_release = package_service.get_latest_release_for_package(self.package.id) if self.latest_release: self.latest_version = self.latest_release.version_text diff --git a/app/ch16_mongodb/final/pypi_org/viewmodels/seo/sitemap_viewmodel.py b/app/ch16_mongodb/final/pypi_org/viewmodels/seo/sitemap_viewmodel.py index 912c4df0..71df2f99 100644 --- a/app/ch16_mongodb/final/pypi_org/viewmodels/seo/sitemap_viewmodel.py +++ b/app/ch16_mongodb/final/pypi_org/viewmodels/seo/sitemap_viewmodel.py @@ -8,5 +8,5 @@ class SiteMapViewModel(ViewModelBase): def __init__(self, limit: int): super().__init__() self.packages = package_service.all_packages(limit) - self.last_updated_text = "2019-07-15" - self.site = "{}://{}".format(flask.request.scheme, flask.request.host) + self.last_updated_text = '2019-07-15' + self.site = '{}://{}'.format(flask.request.scheme, flask.request.host) diff --git a/app/ch16_mongodb/final/pypi_org/views/account_views.py b/app/ch16_mongodb/final/pypi_org/views/account_views.py index 8697cc24..652f43b3 100644 --- a/app/ch16_mongodb/final/pypi_org/views/account_views.py +++ b/app/ch16_mongodb/final/pypi_org/views/account_views.py @@ -25,6 +25,7 @@ def index(): # ################### REGISTER ################################# + @blueprint.route('/account/register', methods=['GET']) @response(template_file='account/register.html') def register_get(): @@ -54,10 +55,16 @@ def register_post(): # ################### LOGIN ################################# + @blueprint.route('/account/login', methods=['GET']) @response(template_file='account/login.html') def login_get(): vm = LoginViewModel() + + # Added after recording, see https://github.com/talkpython/data-driven-web-apps-with-flask/issues/24 + if vm.user_id: + return flask.redirect('/account') + return vm.to_dict() @@ -72,7 +79,7 @@ def login_post(): user = user_service.login_user(vm.email, vm.password) if not user: - vm.error = "The account does not exist or the password is wrong." + vm.error = 'The account does not exist or the password is wrong.' return vm.to_dict() resp = flask.redirect('/account') @@ -83,6 +90,7 @@ def login_post(): # ################### LOGOUT ################################# + @blueprint.route('/account/logout') def logout(): resp = flask.redirect('/') diff --git a/app/ch16_mongodb/final/pypi_org/views/package_views.py b/app/ch16_mongodb/final/pypi_org/views/package_views.py index 1a74549f..2f862740 100644 --- a/app/ch16_mongodb/final/pypi_org/views/package_views.py +++ b/app/ch16_mongodb/final/pypi_org/views/package_views.py @@ -19,4 +19,4 @@ def package_details(package_name: str): @blueprint.route('/') def popular(rank: int): print(type(rank), rank) - return "The details for the {}th most popular package".format(rank) + return 'The details for the {}th most popular package'.format(rank) diff --git a/app/ch16_mongodb/final/pypi_org/views/seo_view.py b/app/ch16_mongodb/final/pypi_org/views/seo_view.py index 93c88de5..f86b6bff 100644 --- a/app/ch16_mongodb/final/pypi_org/views/seo_view.py +++ b/app/ch16_mongodb/final/pypi_org/views/seo_view.py @@ -18,6 +18,7 @@ def sitemap(): # ################### Robots ################################# + @blueprint.route('/robots.txt') @response(mimetype='text/plain', template_file='seo/robots.txt') def robots(): diff --git a/app/ch16_mongodb/final/requirements.piptools b/app/ch16_mongodb/final/requirements.piptools new file mode 100644 index 00000000..22a62e3c --- /dev/null +++ b/app/ch16_mongodb/final/requirements.piptools @@ -0,0 +1,7 @@ +alembic +flask +mongoengine +passlib +progressbar2 +python-dateutil +sqlalchemy diff --git a/app/ch16_mongodb/final/requirements.txt b/app/ch16_mongodb/final/requirements.txt index 5c9d660d..4dbe5e95 100644 --- a/app/ch16_mongodb/final/requirements.txt +++ b/app/ch16_mongodb/final/requirements.txt @@ -1,8 +1,48 @@ -werkzeug -flask -sqlalchemy -mongoengine - -progressbar2 -python-dateutil -passlib +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +alembic==1.14.1 + # via -r requirements.piptools +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +dnspython==2.7.0 + # via pymongo +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +mako==1.3.9 + # via alembic +markupsafe==3.0.2 + # via + # jinja2 + # mako + # werkzeug +mongoengine==0.29.1 + # via -r requirements.piptools +passlib==1.7.4 + # via -r requirements.piptools +progressbar2==4.5.0 + # via -r requirements.piptools +pymongo==4.11.1 + # via mongoengine +python-dateutil==2.9.0.post0 + # via -r requirements.piptools +python-utils==3.9.1 + # via progressbar2 +six==1.17.0 + # via python-dateutil +sqlalchemy==2.0.38 + # via + # -r requirements.piptools + # alembic +typing-extensions==4.12.2 + # via + # alembic + # python-utils + # sqlalchemy +werkzeug==3.1.3 + # via flask diff --git a/app/ch16_mongodb/final/tests/_all_tests.py b/app/ch16_mongodb/final/tests/_all_tests.py index daa69ea4..e5c9b9ea 100644 --- a/app/ch16_mongodb/final/tests/_all_tests.py +++ b/app/ch16_mongodb/final/tests/_all_tests.py @@ -1,17 +1,18 @@ import sys import os -container_folder = os.path.abspath(os.path.join( - os.path.dirname(__file__), '..' -)) +container_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, container_folder) # noinspection PyUnresolvedReferences from account_tests import * + # noinspection PyUnresolvedReferences from package_tests import * + # noinspection PyUnresolvedReferences from sitemap_tests import * + # noinspection PyUnresolvedReferences from home_tests import * diff --git a/app/ch16_mongodb/final/tests/account_tests.py b/app/ch16_mongodb/final/tests/account_tests.py index 4f5aa156..e31e8c28 100644 --- a/app/ch16_mongodb/final/tests/account_tests.py +++ b/app/ch16_mongodb/final/tests/account_tests.py @@ -7,7 +7,7 @@ def test_example(): - print("Test example...") + print('Test example...') assert 1 + 2 == 3 @@ -15,11 +15,7 @@ def test_vm_register_validation_when_valid(): # 3 A's of test: Arrange, Act, then Assert # Arrange - form_data = { - 'name': 'Michael', - 'email': 'michael@talkpython.fm', - 'password': 'a' * 6 - } + form_data = {'name': 'Michael', 'email': 'michael@talkpython.fm', 'password': 'a' * 6} with flask_app.test_request_context(path='/account/register', data=form_data): vm = RegisterViewModel() @@ -37,11 +33,7 @@ def test_vm_register_validation_for_existing_user(): # 3 A's of test: Arrange, Act, then Assert # Arrange - form_data = { - 'name': 'Michael', - 'email': 'michael@talkpython.fm', - 'password': 'a' * 6 - } + form_data = {'name': 'Michael', 'email': 'michael@talkpython.fm', 'password': 'a' * 6} with flask_app.test_request_context(path='/account/register', data=form_data): vm = RegisterViewModel() @@ -62,11 +54,8 @@ def test_v_register_view_new_user(): # Arrange from pypi_org.views.account_views import register_post - form_data = { - 'name': 'Michael', - 'email': 'michael@talkpython.fm', - 'password': 'a' * 6 - } + + form_data = {'name': 'Michael', 'email': 'michael@talkpython.fm', 'password': 'a' * 6} target = 'pypi_org.services.user_service.find_user_by_email' find_user = unittest.mock.patch(target, return_value=None) diff --git a/app/ch16_mongodb/final/tests/package_tests.py b/app/ch16_mongodb/final/tests/package_tests.py index efd04ef5..a7c221a5 100644 --- a/app/ch16_mongodb/final/tests/package_tests.py +++ b/app/ch16_mongodb/final/tests/package_tests.py @@ -12,15 +12,14 @@ def test_package_details_success(): test_package = Package() test_package.id = 'sqlalchemy' - test_package.description = "TDB" + test_package.description = 'TDB' test_package.releases = [ Release(created_date=datetime.datetime.now(), major_ver=1, minor_ver=2, build_ver=200), Release(created_date=datetime.datetime.now() - datetime.timedelta(days=10)), ] # Act - with unittest.mock.patch('pypi_org.services.package_service.get_package_by_id', - return_value=test_package): + with unittest.mock.patch('pypi_org.services.package_service.get_package_by_id', return_value=test_package): with flask_app.test_request_context(path='/project/' + test_package.id): resp: Response = package_details(test_package.id) @@ -33,8 +32,7 @@ def test_package_details_404(client): bad_package_url = 'sqlalchemy_missing' # Act - with unittest.mock.patch('pypi_org.services.package_service.get_package_by_id', - return_value=None): + with unittest.mock.patch('pypi_org.services.package_service.get_package_by_id', return_value=None): resp: Response = client.get(bad_package_url) assert resp.status_code == 404 diff --git a/app/ch16_mongodb/final/tests/sitemap_tests.py b/app/ch16_mongodb/final/tests/sitemap_tests.py index b1afe0fe..4494c30a 100644 --- a/app/ch16_mongodb/final/tests/sitemap_tests.py +++ b/app/ch16_mongodb/final/tests/sitemap_tests.py @@ -10,10 +10,7 @@ def test_int_site_mapped_urls(client): href.text.strip().replace('http://127.0.0.1:5000', '').replace('http://localhost', '') for href in list(x.findall('url/loc')) ] - urls = [ - u if u else '/' - for u in urls - ] + urls = [u if u else '/' for u in urls] print('Testing {} urls from sitemap...'.format(len(urls)), flush=True) has_tested_projects = False @@ -40,7 +37,7 @@ def get_sitemap_text(client): # # ... # - res: Response = client.get("/sitemap.xml") - text = res.data.decode("utf-8") + res: Response = client.get('/sitemap.xml') + text = res.data.decode('utf-8') text = text.replace('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"', '') return text diff --git a/app/ch16_mongodb/final/tests/test_client.py b/app/ch16_mongodb/final/tests/test_client.py index ffbea486..4390409a 100644 --- a/app/ch16_mongodb/final/tests/test_client.py +++ b/app/ch16_mongodb/final/tests/test_client.py @@ -4,9 +4,7 @@ import sys import os -container_folder = os.path.abspath(os.path.join( - os.path.dirname(__file__), '..' -)) +container_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, container_folder) import pypi_org.app diff --git a/app/ch16_mongodb/starter/alembic/alembic_helpers.py b/app/ch16_mongodb/starter/alembic/alembic_helpers.py index 561f7a70..6aea52d4 100644 --- a/app/ch16_mongodb/starter/alembic/alembic_helpers.py +++ b/app/ch16_mongodb/starter/alembic/alembic_helpers.py @@ -5,8 +5,7 @@ def table_has_column(table, column): config = op.get_context().config - engine = engine_from_config( - config.get_section(config.config_ini_section), prefix='sqlalchemy.') + engine = engine_from_config(config.get_section(config.config_ini_section), prefix='sqlalchemy.') insp = reflection.Inspector.from_engine(engine) has_column = False for col in insp.get_columns(table): diff --git a/app/ch16_mongodb/starter/alembic/env.py b/app/ch16_mongodb/starter/alembic/env.py index 27ec49fb..1fdc9cab 100644 --- a/app/ch16_mongodb/starter/alembic/env.py +++ b/app/ch16_mongodb/starter/alembic/env.py @@ -26,6 +26,7 @@ sys.path.insert(0, folder) from pypi_org.data.modelbase import SqlAlchemyBase + # noinspection PyUnresolvedReferences import pypi_org.data.__all_models @@ -49,10 +50,8 @@ def run_migrations_offline(): script output. """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, target_metadata=target_metadata, literal_binds=True - ) + url = config.get_main_option('sqlalchemy.url') + context.configure(url=url, target_metadata=target_metadata, literal_binds=True) with context.begin_transaction(): context.run_migrations() @@ -67,14 +66,12 @@ def run_migrations_online(): """ connectable = engine_from_config( config.get_section(config.config_ini_section), - prefix="sqlalchemy.", + prefix='sqlalchemy.', poolclass=pool.NullPool, ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() diff --git a/app/ch16_mongodb/starter/alembic/versions/722c82f0097c_added_auditing_table.py b/app/ch16_mongodb/starter/alembic/versions/722c82f0097c_added_auditing_table.py index 2c3e2a39..7c6e892f 100644 --- a/app/ch16_mongodb/starter/alembic/versions/722c82f0097c_added_auditing_table.py +++ b/app/ch16_mongodb/starter/alembic/versions/722c82f0097c_added_auditing_table.py @@ -18,11 +18,12 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('auditing', - sa.Column('id', sa.String(), nullable=False), - sa.Column('created_date', sa.DateTime(), nullable=True), - sa.Column('description', sa.String(), nullable=True), - sa.PrimaryKeyConstraint('id') + op.create_table( + 'auditing', + sa.Column('id', sa.String(), nullable=False), + sa.Column('created_date', sa.DateTime(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id'), ) op.create_index(op.f('ix_auditing_created_date'), 'auditing', ['created_date'], unique=False) # ### end Alembic commands ### diff --git a/app/ch16_mongodb/starter/pypi_org/app.py b/app/ch16_mongodb/starter/pypi_org/app.py index d1de87c6..ee364816 100644 --- a/app/ch16_mongodb/starter/pypi_org/app.py +++ b/app/ch16_mongodb/starter/pypi_org/app.py @@ -16,21 +16,18 @@ def main(): def configure(): - print("Configuring Flask app:") + print('Configuring Flask app:') register_blueprints() - print("Registered blueprints") + print('Registered blueprints') setup_db() - print("DB setup completed.") - print("", flush=True) + print('DB setup completed.') + print('', flush=True) def setup_db(): - db_file = os.path.join( - os.path.dirname(__file__), - 'db', - 'pypi.sqlite') + db_file = os.path.join(os.path.dirname(__file__), 'db', 'pypi.sqlite') db_session.global_init(db_file) diff --git a/app/ch16_mongodb/starter/pypi_org/bin/basic_inserts.py b/app/ch16_mongodb/starter/pypi_org/bin/basic_inserts.py index 3a2f360d..6d04f551 100644 --- a/app/ch16_mongodb/starter/pypi_org/bin/basic_inserts.py +++ b/app/ch16_mongodb/starter/pypi_org/bin/basic_inserts.py @@ -1,14 +1,13 @@ import os import sys +# Make it run more easily outside of PyCharm +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + import pypi_org.data.db_session as db_session from pypi_org.data.package import Package from pypi_org.data.releases import Release -# Make it run more easily outside of PyCharm -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) - def main(): init_db() @@ -20,24 +19,24 @@ def insert_a_package(): p = Package() p.id = input('Package id / name: ').strip().lower() - p.summary = input("Package summary: ").strip() - p.author_name = input("Author: ").strip() - p.license = input("License: ").strip() + p.summary = input('Package summary: ').strip() + p.author_name = input('Author: ').strip() + p.license = input('License: ').strip() - print("Release 1:") + print('Release 1:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) - print("Release 2:") + print('Release 2:') r = Release() - r.major_ver = int(input("Major version: ")) - r.minor_ver = int(input("Minor version: ")) - r.build_ver = int(input("Build version: ")) - r.size = int(input("Size in bytes: ")) + r.major_ver = int(input('Major version: ')) + r.minor_ver = int(input('Minor version: ')) + r.build_ver = int(input('Build version: ')) + r.size = int(input('Size in bytes: ')) p.releases.append(r) session = db_session.create_session() diff --git a/app/ch16_mongodb/starter/pypi_org/bin/load_data.py b/app/ch16_mongodb/starter/pypi_org/bin/load_data.py index 15237f18..228c1b6b 100644 --- a/app/ch16_mongodb/starter/pypi_org/bin/load_data.py +++ b/app/ch16_mongodb/starter/pypi_org/bin/load_data.py @@ -7,8 +7,7 @@ import progressbar from dateutil.parser import parse -sys.path.insert(0, os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", ".."))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) import pypi_org.data.db_session as db_session from pypi_org.data.languages import ProgrammingLanguage @@ -39,7 +38,7 @@ def main(): def do_import_languages(file_data: List[dict]): imported = set() - print("Importing languages ... ", flush=True) + print('Importing languages ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -74,7 +73,7 @@ def do_import_languages(file_data: List[dict]): def do_import_licenses(file_data: List[dict]): imported = set() - print("Importing licenses ... ", flush=True) + print('Importing licenses ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): info = p.get('info') @@ -100,17 +99,17 @@ def do_import_licenses(file_data: List[dict]): def do_summary(): session = db_session.create_session() - print("Final numbers:") - print("Users: {:,}".format(session.query(User).count())) - print("Packages: {:,}".format(session.query(Package).count())) - print("Releases: {:,}".format(session.query(Release).count())) - print("Maintainers: {:,}".format(session.query(Maintainer).count())) - print("Languages: {:,}".format(session.query(ProgrammingLanguage).count())) - print("Licenses: {:,}".format(session.query(License).count())) + print('Final numbers:') + print('Users: {:,}'.format(session.query(User).count())) + print('Packages: {:,}'.format(session.query(Package).count())) + print('Releases: {:,}'.format(session.query(Release).count())) + print('Maintainers: {:,}'.format(session.query(Maintainer).count())) + print('Languages: {:,}'.format(session.query(ProgrammingLanguage).count())) + print('Licenses: {:,}'.format(session.query(License).count())) def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: - print("Importing users ... ", flush=True) + print('Importing users ... ', flush=True) with progressbar.ProgressBar(max_value=len(user_lookup)) as bar: for idx, (email, name) in enumerate(user_lookup.items()): session = db_session.create_session() @@ -134,29 +133,29 @@ def do_user_import(user_lookup: Dict[str, str]) -> Dict[str, User]: def do_import_packages(file_data: List[dict], user_lookup: Dict[str, User]): errored_packages = [] - print("Importing packages and releases ... ", flush=True) + print('Importing packages and releases ... ', flush=True) with progressbar.ProgressBar(max_value=len(file_data)) as bar: for idx, p in enumerate(file_data): try: load_package(p, user_lookup) bar.update(idx) except Exception as x: - errored_packages.append((p, " *** Errored out for package {}, {}".format(p.get('package_name'), x))) + errored_packages.append((p, ' *** Errored out for package {}, {}'.format(p.get('package_name'), x))) raise sys.stderr.flush() sys.stdout.flush() print() - print("Completed packages with {} errors.".format(len(errored_packages))) - for (p, txt) in errored_packages: + print('Completed packages with {} errors.'.format(len(errored_packages))) + for p, txt in errored_packages: print(txt) def do_load_files() -> List[dict]: data_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../../data/pypi-top-100')) - print("Loading files from {}".format(data_path)) + print('Loading files from {}'.format(data_path)) files = get_file_names(data_path) - print("Found {:,} files, loading ...".format(len(files)), flush=True) - time.sleep(.1) + print('Found {:,} files, loading ...'.format(len(files)), flush=True) + time.sleep(0.1) file_data = [] with progressbar.ProgressBar(max_value=len(files)) as bar: @@ -171,7 +170,7 @@ def do_load_files() -> List[dict]: def find_users(data: List[dict]) -> dict: - print("Discovering users...", flush=True) + print('Discovering users...', flush=True) found_users = {} with progressbar.ProgressBar(max_value=len(data)) as bar: @@ -184,7 +183,7 @@ def find_users(data: List[dict]) -> dict: sys.stderr.flush() sys.stdout.flush() print() - print("Discovered {:,} users".format(len(found_users))) + print('Discovered {:,} users'.format(len(found_users))) print() return found_users @@ -215,7 +214,7 @@ def load_file_data(filename: str) -> dict: with open(filename, 'r', encoding='utf-8') as fin: data = json.load(fin) except Exception as x: - print("ERROR in file: {}, details: {}".format(filename, x), flush=True) + print('ERROR in file: {}, details: {}'.format(filename, x), flush=True) raise return data @@ -233,7 +232,7 @@ def load_package(data: dict, user_lookup: Dict[str, User]): p.author = info.get('author') p.author_email = info.get('author_email') - releases = build_releases(p.id, data.get("releases", {})) + releases = build_releases(p.id, data.get('releases', {})) if releases: p.created_date = releases[0].created_date @@ -283,18 +282,13 @@ def detect_license(license_text: str) -> Optional[str]: license_text = license_text.strip() if len(license_text) > 100 or '\n' in license_text: - return "CUSTOM" + return 'CUSTOM' - license_text = license_text \ - .replace('Software License', '') \ - .replace('License', '') + license_text = license_text.replace('Software License', '').replace('License', '') if '::' in license_text: # E.g. 'License :: OSI Approved :: Apache Software License' - return license_text \ - .split(':')[-1] \ - .replace(' ', ' ') \ - .strip() + return license_text.split(':')[-1].replace(' ', ' ').strip() return license_text.strip() @@ -339,11 +333,11 @@ def make_version_num(version_text): return major, minor, build -def try_int(text) -> int: +def try_int(text) -> Optional[int]: try: return int(text) except: - return 0 + return None def init_db(): @@ -357,9 +351,7 @@ def get_file_names(data_path: str) -> List[str]: files = [] for f in os.listdir(data_path): if f.endswith('.json'): - files.append( - os.path.abspath(os.path.join(data_path, f)) - ) + files.append(os.path.abspath(os.path.join(data_path, f))) files.sort() return files diff --git a/app/ch16_mongodb/starter/pypi_org/data/__all_models.py b/app/ch16_mongodb/starter/pypi_org/data/__all_models.py index f399ef54..ca4804ac 100644 --- a/app/ch16_mongodb/starter/pypi_org/data/__all_models.py +++ b/app/ch16_mongodb/starter/pypi_org/data/__all_models.py @@ -5,17 +5,24 @@ # noinspection PyUnresolvedReferences import pypi_org.data.audit + # noinspection PyUnresolvedReferences import pypi_org.data.downloads + # noinspection PyUnresolvedReferences import pypi_org.data.languages + # noinspection PyUnresolvedReferences import pypi_org.data.licenses + # noinspection PyUnresolvedReferences import pypi_org.data.maintainers + # noinspection PyUnresolvedReferences import pypi_org.data.package + # noinspection PyUnresolvedReferences import pypi_org.data.releases + # noinspection PyUnresolvedReferences import pypi_org.data.users diff --git a/app/ch16_mongodb/starter/pypi_org/data/db_session.py b/app/ch16_mongodb/starter/pypi_org/data/db_session.py index acfc485d..a1c4df68 100644 --- a/app/ch16_mongodb/starter/pypi_org/data/db_session.py +++ b/app/ch16_mongodb/starter/pypi_org/data/db_session.py @@ -14,10 +14,10 @@ def global_init(db_file: str): return if not db_file or not db_file.strip(): - raise Exception("You must specify a db file.") + raise Exception('You must specify a db file.') conn_str = 'sqlite:///' + db_file.strip() - print("Connecting to DB with {}".format(conn_str)) + print('Connecting to DB with {}'.format(conn_str)) engine = sa.create_engine(conn_str, echo=False) __factory = orm.sessionmaker(bind=engine) diff --git a/app/ch16_mongodb/starter/pypi_org/data/downloads.py b/app/ch16_mongodb/starter/pypi_org/data/downloads.py index 66068f02..dd8afd65 100644 --- a/app/ch16_mongodb/starter/pypi_org/data/downloads.py +++ b/app/ch16_mongodb/starter/pypi_org/data/downloads.py @@ -7,8 +7,7 @@ class Download(SqlAlchemyBase): __tablename__ = 'downloads' id: int = sqlalchemy.Column(sqlalchemy.BigInteger, primary_key=True, autoincrement=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) package_id: str = sqlalchemy.Column(sqlalchemy.String, index=True) release_id: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) diff --git a/app/ch16_mongodb/starter/pypi_org/data/languages.py b/app/ch16_mongodb/starter/pypi_org/data/languages.py index 964b8444..3a73dde0 100644 --- a/app/ch16_mongodb/starter/pypi_org/data/languages.py +++ b/app/ch16_mongodb/starter/pypi_org/data/languages.py @@ -7,6 +7,5 @@ class ProgrammingLanguage(SqlAlchemyBase): __tablename__ = 'languages' id: str = sqlalchemy.Column(sqlalchemy.String, primary_key=True) - created_date: datetime.datetime = sqlalchemy.Column( - sqlalchemy.DateTime, default=datetime.datetime.now, index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) description: str = sqlalchemy.Column(sqlalchemy.String) diff --git a/app/ch16_mongodb/starter/pypi_org/data/package.py b/app/ch16_mongodb/starter/pypi_org/data/package.py index c3420d1b..e0e48878 100644 --- a/app/ch16_mongodb/starter/pypi_org/data/package.py +++ b/app/ch16_mongodb/starter/pypi_org/data/package.py @@ -3,6 +3,8 @@ import sqlalchemy as sa import sqlalchemy.orm as orm +from sqlalchemy.orm import Mapped + from pypi_org.data.modelbase import SqlAlchemyBase from pypi_org.data.releases import Release @@ -12,7 +14,6 @@ class Package(SqlAlchemyBase): id: str = sa.Column(sa.String, primary_key=True) created_date: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) - last_updated: datetime.datetime = sa.Column(sa.DateTime, default=datetime.datetime.now, index=True) summary: str = sa.Column(sa.String, nullable=False) description: str = sa.Column(sa.String, nullable=True) @@ -26,11 +27,15 @@ class Package(SqlAlchemyBase): license: str = sa.Column(sa.String, index=True) # releases relationship - releases: List[Release] = orm.relation("Release", order_by=[ - Release.major_ver.desc(), - Release.minor_ver.desc(), - Release.build_ver.desc(), - ], back_populates='package') + releases: Mapped[Release] = orm.relationship( + 'Release', + order_by=[ + Release.major_ver.desc(), + Release.minor_ver.desc(), + Release.build_ver.desc(), + ], + back_populates='package', + ) def __repr__(self): return ''.format(self.id) diff --git a/app/ch16_mongodb/starter/pypi_org/data/releases.py b/app/ch16_mongodb/starter/pypi_org/data/releases.py index 7650e438..4b2db2bf 100644 --- a/app/ch16_mongodb/starter/pypi_org/data/releases.py +++ b/app/ch16_mongodb/starter/pypi_org/data/releases.py @@ -13,16 +13,14 @@ class Release(SqlAlchemyBase): minor_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) build_ver: int = sqlalchemy.Column(sqlalchemy.BigInteger, index=True) - created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, - default=datetime.datetime.now, - index=True) + created_date: datetime.datetime = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now, index=True) comment: str = sqlalchemy.Column(sqlalchemy.String) url: str = sqlalchemy.Column(sqlalchemy.String) size: int = sqlalchemy.Column(sqlalchemy.BigInteger) # Package relationship - package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey("packages.id")) - package = orm.relation('Package') + package_id: str = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey('packages.id')) + package = orm.relationship('Package') @property def version_text(self): diff --git a/app/ch16_mongodb/starter/pypi_org/infrastructure/cookie_auth.py b/app/ch16_mongodb/starter/pypi_org/infrastructure/cookie_auth.py index 64dc85a7..c215c0af 100644 --- a/app/ch16_mongodb/starter/pypi_org/infrastructure/cookie_auth.py +++ b/app/ch16_mongodb/starter/pypi_org/infrastructure/cookie_auth.py @@ -1,19 +1,18 @@ import hashlib -from datetime import timedelta from typing import Optional from flask import Request from flask import Response -from pypi_org.bin.load_data import try_int +from pypi_org.infrastructure.num_convert import try_int auth_cookie_name = 'pypi_demo_user' def set_auth(response: Response, user_id: int): hash_val = __hash_text(str(user_id)) - val = "{}:{}".format(user_id, hash_val) - response.set_cookie(auth_cookie_name, val) + val = '{}:{}'.format(user_id, hash_val) + response.set_cookie(auth_cookie_name, val, secure=False, httponly=True, samesite='Lax') def __hash_text(text: str) -> str: @@ -21,10 +20,6 @@ def __hash_text(text: str) -> str: return hashlib.sha512(text.encode('utf-8')).hexdigest() -def __add_cookie_callback(_, response: Response, name: str, value: str): - response.set_cookie(name, value, max_age=timedelta(days=30)) - - def get_user_id_via_auth_cookie(request: Request) -> Optional[int]: if auth_cookie_name not in request.cookies: return None @@ -38,7 +33,7 @@ def get_user_id_via_auth_cookie(request: Request) -> Optional[int]: hash_val = parts[1] hash_val_check = __hash_text(user_id) if hash_val != hash_val_check: - print("Warning: Hash mismatch, invalid cookie value") + print('Warning: Hash mismatch, invalid cookie value') return None return try_int(user_id) diff --git a/app/ch16_mongodb/starter/pypi_org/infrastructure/num_convert.py b/app/ch16_mongodb/starter/pypi_org/infrastructure/num_convert.py new file mode 100644 index 00000000..4e1d8879 --- /dev/null +++ b/app/ch16_mongodb/starter/pypi_org/infrastructure/num_convert.py @@ -0,0 +1,8 @@ +from typing import Optional + + +def try_int(text) -> Optional[int]: + try: + return int(text) + except: + return None diff --git a/app/ch16_mongodb/starter/pypi_org/infrastructure/request_dict.py b/app/ch16_mongodb/starter/pypi_org/infrastructure/request_dict.py index 495fd979..d44ae574 100644 --- a/app/ch16_mongodb/starter/pypi_org/infrastructure/request_dict.py +++ b/app/ch16_mongodb/starter/pypi_org/infrastructure/request_dict.py @@ -1,4 +1,5 @@ import flask +from werkzeug.datastructures import MultiDict class RequestDictionary(dict): @@ -13,11 +14,22 @@ def __getattr__(self, key): def create(default_val=None, **route_args) -> RequestDictionary: request = flask.request + # Adding this retro actively. Some folks are experiencing issues where they + # are getting a list rather than plain dict. I think it's from multiple + # entries in the multidict. This should fix it. + args = request.args + if isinstance(request.args, MultiDict): + args = request.args.to_dict() + + form = request.form + if isinstance(request.args, MultiDict): + form = request.form.to_dict() + data = { - **request.args, # The key/value pairs in the URL query string + **args, # The key/value pairs in the URL query string **request.headers, # Header values - **request.form, # The key/value pairs in the body, from a HTML post form - **route_args # And additional arguments the method access, if they want them merged. + **form, # The key/value pairs in the body, from a HTML post form + **route_args, # And additional arguments the method access, if they want them merged. } return RequestDictionary(data, default_val=default_val) diff --git a/app/ch16_mongodb/starter/pypi_org/infrastructure/view_modifiers.py b/app/ch16_mongodb/starter/pypi_org/infrastructure/view_modifiers.py index 82ce5baf..d944c142 100644 --- a/app/ch16_mongodb/starter/pypi_org/infrastructure/view_modifiers.py +++ b/app/ch16_mongodb/starter/pypi_org/infrastructure/view_modifiers.py @@ -1,7 +1,8 @@ from functools import wraps import flask -import werkzeug.wrappers.response +import werkzeug +import werkzeug.wrappers def response(*, mimetype: str = None, template_file: str = None): @@ -11,9 +12,11 @@ def response_inner(f): @wraps(f) def view_method(*args, **kwargs): response_val = f(*args, **kwargs) - if isinstance(response_val, flask.Response): + + if isinstance(response_val, werkzeug.wrappers.Response): return response_val - if isinstance(response_val, werkzeug.wrappers.response.Response): + + if isinstance(response_val, flask.Response): return response_val if isinstance(response_val, dict): @@ -23,7 +26,8 @@ def view_method(*args, **kwargs): if template_file and not isinstance(response_val, dict): raise Exception( - "Invalid return type {}, we expected a dict as the return value.".format(type(response_val))) + 'Invalid return type {}, we expected a dict as the return value.'.format(type(response_val)) + ) if template_file: response_val = flask.render_template(template_file, **response_val) @@ -39,6 +43,7 @@ def view_method(*args, **kwargs): return response_inner + # # def template(template_file: str = None): # def template_inner(f): diff --git a/app/ch16_mongodb/starter/pypi_org/services/package_service.py b/app/ch16_mongodb/starter/pypi_org/services/package_service.py index b03408d2..9a153773 100644 --- a/app/ch16_mongodb/starter/pypi_org/services/package_service.py +++ b/app/ch16_mongodb/starter/pypi_org/services/package_service.py @@ -10,12 +10,13 @@ def get_latest_releases(limit=10) -> List[Release]: session = db_session.create_session() try: - - releases = session.query(Release). \ - options(sqlalchemy.orm.joinedload(Release.package)). \ - order_by(Release.created_date.desc()). \ - limit(limit). \ - all() + releases = ( + session.query(Release) + .options(sqlalchemy.orm.joinedload(Release.package)) + .order_by(Release.created_date.desc()) + .limit(limit) + .all() + ) finally: session.close() @@ -47,11 +48,12 @@ def get_package_by_id(package_id: str) -> Optional[Package]: session = db_session.create_session() try: - - package = session.query(Package) \ - .options(sqlalchemy.orm.joinedload(Package.releases)) \ - .filter(Package.id == package_id) \ + package = ( + session.query(Package) + .options(sqlalchemy.orm.joinedload(Package.releases)) + .filter(Package.id == package_id) .first() + ) finally: session.close() diff --git a/app/ch16_mongodb/starter/pypi_org/viewmodels/cms/page_viewmodel.py b/app/ch16_mongodb/starter/pypi_org/viewmodels/cms/page_viewmodel.py index b7bb3249..ec033c3f 100644 --- a/app/ch16_mongodb/starter/pypi_org/viewmodels/cms/page_viewmodel.py +++ b/app/ch16_mongodb/starter/pypi_org/viewmodels/cms/page_viewmodel.py @@ -7,4 +7,3 @@ def __init__(self, full_url: str): super().__init__() self.page = cms_service.get_page(full_url) - diff --git a/app/ch16_mongodb/starter/pypi_org/viewmodels/packages/pagedetails_viewmodel.py b/app/ch16_mongodb/starter/pypi_org/viewmodels/packages/pagedetails_viewmodel.py index 63c4cf68..09f47c6f 100644 --- a/app/ch16_mongodb/starter/pypi_org/viewmodels/packages/pagedetails_viewmodel.py +++ b/app/ch16_mongodb/starter/pypi_org/viewmodels/packages/pagedetails_viewmodel.py @@ -11,7 +11,7 @@ def __init__(self, package_name: str): self.package_name = package_name.strip().lower() self.package = package_service.get_package_by_id(self.package_name) - self.latest_version = "0.0.0" + self.latest_version = '0.0.0' self.latest_release = None self.is_latest = True diff --git a/app/ch16_mongodb/starter/pypi_org/viewmodels/seo/sitemap_viewmodel.py b/app/ch16_mongodb/starter/pypi_org/viewmodels/seo/sitemap_viewmodel.py index 912c4df0..71df2f99 100644 --- a/app/ch16_mongodb/starter/pypi_org/viewmodels/seo/sitemap_viewmodel.py +++ b/app/ch16_mongodb/starter/pypi_org/viewmodels/seo/sitemap_viewmodel.py @@ -8,5 +8,5 @@ class SiteMapViewModel(ViewModelBase): def __init__(self, limit: int): super().__init__() self.packages = package_service.all_packages(limit) - self.last_updated_text = "2019-07-15" - self.site = "{}://{}".format(flask.request.scheme, flask.request.host) + self.last_updated_text = '2019-07-15' + self.site = '{}://{}'.format(flask.request.scheme, flask.request.host) diff --git a/app/ch16_mongodb/starter/pypi_org/views/account_views.py b/app/ch16_mongodb/starter/pypi_org/views/account_views.py index 8697cc24..652f43b3 100644 --- a/app/ch16_mongodb/starter/pypi_org/views/account_views.py +++ b/app/ch16_mongodb/starter/pypi_org/views/account_views.py @@ -25,6 +25,7 @@ def index(): # ################### REGISTER ################################# + @blueprint.route('/account/register', methods=['GET']) @response(template_file='account/register.html') def register_get(): @@ -54,10 +55,16 @@ def register_post(): # ################### LOGIN ################################# + @blueprint.route('/account/login', methods=['GET']) @response(template_file='account/login.html') def login_get(): vm = LoginViewModel() + + # Added after recording, see https://github.com/talkpython/data-driven-web-apps-with-flask/issues/24 + if vm.user_id: + return flask.redirect('/account') + return vm.to_dict() @@ -72,7 +79,7 @@ def login_post(): user = user_service.login_user(vm.email, vm.password) if not user: - vm.error = "The account does not exist or the password is wrong." + vm.error = 'The account does not exist or the password is wrong.' return vm.to_dict() resp = flask.redirect('/account') @@ -83,6 +90,7 @@ def login_post(): # ################### LOGOUT ################################# + @blueprint.route('/account/logout') def logout(): resp = flask.redirect('/') diff --git a/app/ch16_mongodb/starter/pypi_org/views/package_views.py b/app/ch16_mongodb/starter/pypi_org/views/package_views.py index 1a74549f..2f862740 100644 --- a/app/ch16_mongodb/starter/pypi_org/views/package_views.py +++ b/app/ch16_mongodb/starter/pypi_org/views/package_views.py @@ -19,4 +19,4 @@ def package_details(package_name: str): @blueprint.route('/') def popular(rank: int): print(type(rank), rank) - return "The details for the {}th most popular package".format(rank) + return 'The details for the {}th most popular package'.format(rank) diff --git a/app/ch16_mongodb/starter/pypi_org/views/seo_view.py b/app/ch16_mongodb/starter/pypi_org/views/seo_view.py index 93c88de5..f86b6bff 100644 --- a/app/ch16_mongodb/starter/pypi_org/views/seo_view.py +++ b/app/ch16_mongodb/starter/pypi_org/views/seo_view.py @@ -18,6 +18,7 @@ def sitemap(): # ################### Robots ################################# + @blueprint.route('/robots.txt') @response(mimetype='text/plain', template_file='seo/robots.txt') def robots(): diff --git a/app/ch16_mongodb/starter/requirements.piptools b/app/ch16_mongodb/starter/requirements.piptools new file mode 100644 index 00000000..d13bf0c8 --- /dev/null +++ b/app/ch16_mongodb/starter/requirements.piptools @@ -0,0 +1,6 @@ +alembic +flask +passlib +progressbar2 +python-dateutil +sqlalchemy diff --git a/app/ch16_mongodb/starter/requirements.txt b/app/ch16_mongodb/starter/requirements.txt index e10973d8..19fc8c45 100644 --- a/app/ch16_mongodb/starter/requirements.txt +++ b/app/ch16_mongodb/starter/requirements.txt @@ -1,8 +1,42 @@ -werkzeug -flask -sqlalchemy - -progressbar2 -python-dateutil -passlib - +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +alembic==1.14.1 + # via -r requirements.piptools +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +mako==1.3.9 + # via alembic +markupsafe==3.0.2 + # via + # jinja2 + # mako + # werkzeug +passlib==1.7.4 + # via -r requirements.piptools +progressbar2==4.5.0 + # via -r requirements.piptools +python-dateutil==2.9.0.post0 + # via -r requirements.piptools +python-utils==3.9.1 + # via progressbar2 +six==1.17.0 + # via python-dateutil +sqlalchemy==2.0.38 + # via + # -r requirements.piptools + # alembic +typing-extensions==4.12.2 + # via + # alembic + # python-utils + # sqlalchemy +werkzeug==3.1.3 + # via flask diff --git a/app/ch16_mongodb/starter/tests/_all_tests.py b/app/ch16_mongodb/starter/tests/_all_tests.py index daa69ea4..e5c9b9ea 100644 --- a/app/ch16_mongodb/starter/tests/_all_tests.py +++ b/app/ch16_mongodb/starter/tests/_all_tests.py @@ -1,17 +1,18 @@ import sys import os -container_folder = os.path.abspath(os.path.join( - os.path.dirname(__file__), '..' -)) +container_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, container_folder) # noinspection PyUnresolvedReferences from account_tests import * + # noinspection PyUnresolvedReferences from package_tests import * + # noinspection PyUnresolvedReferences from sitemap_tests import * + # noinspection PyUnresolvedReferences from home_tests import * diff --git a/app/ch16_mongodb/starter/tests/account_tests.py b/app/ch16_mongodb/starter/tests/account_tests.py index 4f5aa156..e31e8c28 100644 --- a/app/ch16_mongodb/starter/tests/account_tests.py +++ b/app/ch16_mongodb/starter/tests/account_tests.py @@ -7,7 +7,7 @@ def test_example(): - print("Test example...") + print('Test example...') assert 1 + 2 == 3 @@ -15,11 +15,7 @@ def test_vm_register_validation_when_valid(): # 3 A's of test: Arrange, Act, then Assert # Arrange - form_data = { - 'name': 'Michael', - 'email': 'michael@talkpython.fm', - 'password': 'a' * 6 - } + form_data = {'name': 'Michael', 'email': 'michael@talkpython.fm', 'password': 'a' * 6} with flask_app.test_request_context(path='/account/register', data=form_data): vm = RegisterViewModel() @@ -37,11 +33,7 @@ def test_vm_register_validation_for_existing_user(): # 3 A's of test: Arrange, Act, then Assert # Arrange - form_data = { - 'name': 'Michael', - 'email': 'michael@talkpython.fm', - 'password': 'a' * 6 - } + form_data = {'name': 'Michael', 'email': 'michael@talkpython.fm', 'password': 'a' * 6} with flask_app.test_request_context(path='/account/register', data=form_data): vm = RegisterViewModel() @@ -62,11 +54,8 @@ def test_v_register_view_new_user(): # Arrange from pypi_org.views.account_views import register_post - form_data = { - 'name': 'Michael', - 'email': 'michael@talkpython.fm', - 'password': 'a' * 6 - } + + form_data = {'name': 'Michael', 'email': 'michael@talkpython.fm', 'password': 'a' * 6} target = 'pypi_org.services.user_service.find_user_by_email' find_user = unittest.mock.patch(target, return_value=None) diff --git a/app/ch16_mongodb/starter/tests/package_tests.py b/app/ch16_mongodb/starter/tests/package_tests.py index efd04ef5..a7c221a5 100644 --- a/app/ch16_mongodb/starter/tests/package_tests.py +++ b/app/ch16_mongodb/starter/tests/package_tests.py @@ -12,15 +12,14 @@ def test_package_details_success(): test_package = Package() test_package.id = 'sqlalchemy' - test_package.description = "TDB" + test_package.description = 'TDB' test_package.releases = [ Release(created_date=datetime.datetime.now(), major_ver=1, minor_ver=2, build_ver=200), Release(created_date=datetime.datetime.now() - datetime.timedelta(days=10)), ] # Act - with unittest.mock.patch('pypi_org.services.package_service.get_package_by_id', - return_value=test_package): + with unittest.mock.patch('pypi_org.services.package_service.get_package_by_id', return_value=test_package): with flask_app.test_request_context(path='/project/' + test_package.id): resp: Response = package_details(test_package.id) @@ -33,8 +32,7 @@ def test_package_details_404(client): bad_package_url = 'sqlalchemy_missing' # Act - with unittest.mock.patch('pypi_org.services.package_service.get_package_by_id', - return_value=None): + with unittest.mock.patch('pypi_org.services.package_service.get_package_by_id', return_value=None): resp: Response = client.get(bad_package_url) assert resp.status_code == 404 diff --git a/app/ch16_mongodb/starter/tests/sitemap_tests.py b/app/ch16_mongodb/starter/tests/sitemap_tests.py index b1afe0fe..4494c30a 100644 --- a/app/ch16_mongodb/starter/tests/sitemap_tests.py +++ b/app/ch16_mongodb/starter/tests/sitemap_tests.py @@ -10,10 +10,7 @@ def test_int_site_mapped_urls(client): href.text.strip().replace('http://127.0.0.1:5000', '').replace('http://localhost', '') for href in list(x.findall('url/loc')) ] - urls = [ - u if u else '/' - for u in urls - ] + urls = [u if u else '/' for u in urls] print('Testing {} urls from sitemap...'.format(len(urls)), flush=True) has_tested_projects = False @@ -40,7 +37,7 @@ def get_sitemap_text(client): # # ... # - res: Response = client.get("/sitemap.xml") - text = res.data.decode("utf-8") + res: Response = client.get('/sitemap.xml') + text = res.data.decode('utf-8') text = text.replace('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"', '') return text diff --git a/app/ch16_mongodb/starter/tests/test_client.py b/app/ch16_mongodb/starter/tests/test_client.py index ffbea486..4390409a 100644 --- a/app/ch16_mongodb/starter/tests/test_client.py +++ b/app/ch16_mongodb/starter/tests/test_client.py @@ -4,9 +4,7 @@ import sys import os -container_folder = os.path.abspath(os.path.join( - os.path.dirname(__file__), '..' -)) +container_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, container_folder) import pypi_org.app diff --git a/requirements.piptools b/requirements.piptools new file mode 100644 index 00000000..22a62e3c --- /dev/null +++ b/requirements.piptools @@ -0,0 +1,7 @@ +alembic +flask +mongoengine +passlib +progressbar2 +python-dateutil +sqlalchemy diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..4dbe5e95 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,48 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.piptools --output-file requirements.txt +alembic==1.14.1 + # via -r requirements.piptools +blinker==1.9.0 + # via flask +click==8.1.8 + # via flask +dnspython==2.7.0 + # via pymongo +flask==3.1.0 + # via -r requirements.piptools +itsdangerous==2.2.0 + # via flask +jinja2==3.1.5 + # via flask +mako==1.3.9 + # via alembic +markupsafe==3.0.2 + # via + # jinja2 + # mako + # werkzeug +mongoengine==0.29.1 + # via -r requirements.piptools +passlib==1.7.4 + # via -r requirements.piptools +progressbar2==4.5.0 + # via -r requirements.piptools +pymongo==4.11.1 + # via mongoengine +python-dateutil==2.9.0.post0 + # via -r requirements.piptools +python-utils==3.9.1 + # via progressbar2 +six==1.17.0 + # via python-dateutil +sqlalchemy==2.0.38 + # via + # -r requirements.piptools + # alembic +typing-extensions==4.12.2 + # via + # alembic + # python-utils + # sqlalchemy +werkzeug==3.1.3 + # via flask diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..5537773d --- /dev/null +++ b/ruff.toml @@ -0,0 +1,42 @@ +# [ruff] +line-length = 120 +format.quote-style = "single" + +# Enable Pyflakes `E` and `F` codes by default. +select = ["E", "F"] +ignore = [] + +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".ruff_cache", + ".svn", + ".tox", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + ".env", + ".venv", + "venv", +] +per-file-ignores = {} + +# Allow unused variables when underscore-prefixed. +# dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +# Assume Python 3.11. +target-version = "py311" + +#[tool.ruff.mccabe] +## Unlike Flake8, default to a complexity level of 10. +mccabe.max-complexity = 10