diff --git a/.gitignore b/.gitignore
index 98d0aa73..e0bf6584 100644
--- a/.gitignore
+++ b/.gitignore
@@ -142,3 +142,40 @@ app/ch11_migrations/final/.idea/dataSources/bb882f32-bdb3-45cb-adce-c46711dd125e
.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
index e5612f37..630ad4c3 100644
--- a/.idea/dictionaries/mkennedy.xml
+++ b/.idea/dictionaries/mkennedy.xml
@@ -7,6 +7,7 @@
passlibpypisqlalchemy
+ tablenamewerkzeug
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 0c0941eb..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
@@ -26,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)
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 0978163e..d944c142 100644
--- a/app/ch06_routing/final/pypi_org/infrastructure/view_modifiers.py
+++ b/app/ch06_routing/final/pypi_org/infrastructure/view_modifiers.py
@@ -26,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)
@@ -42,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 0978163e..d944c142 100644
--- a/app/ch06_routing/starter/pypi_org/infrastructure/view_modifiers.py
+++ b/app/ch06_routing/starter/pypi_org/infrastructure/view_modifiers.py
@@ -26,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)
@@ -42,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 0978163e..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
@@ -26,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)
@@ -42,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 0978163e..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
@@ -26,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)
@@ -42,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 0978163e..d944c142 100644
--- a/app/ch09_sqlalchemy/final/pypi_org/infrastructure/view_modifiers.py
+++ b/app/ch09_sqlalchemy/final/pypi_org/infrastructure/view_modifiers.py
@@ -26,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)
@@ -42,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/infrastructure/view_modifiers.py b/app/ch09_sqlalchemy/starter/pypi_org/infrastructure/view_modifiers.py
index 0978163e..d944c142 100644
--- a/app/ch09_sqlalchemy/starter/pypi_org/infrastructure/view_modifiers.py
+++ b/app/ch09_sqlalchemy/starter/pypi_org/infrastructure/view_modifiers.py
@@ -26,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)
@@ -42,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 ba62ff7d..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,11 +4,13 @@
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
@@ -40,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')
@@ -75,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')
@@ -101,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()
@@ -135,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:
@@ -172,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:
@@ -185,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
@@ -216,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
@@ -234,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
@@ -284,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()
@@ -351,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
index bf88a6bc..4e1d8879 100644
--- a/app/ch10_using_sqlachemy/final/pypi_org/infrastructure/num_convert.py
+++ b/app/ch10_using_sqlachemy/final/pypi_org/infrastructure/num_convert.py
@@ -1,5 +1,8 @@
-def try_int(text) -> int:
+from typing import Optional
+
+
+def try_int(text) -> Optional[int]:
try:
return int(text)
except:
- return 0
+ 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 0978163e..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
@@ -26,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)
@@ -42,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 0978163e..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
@@ -26,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)
@@ -42,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 277a48f8..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,10 +7,10 @@
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__), '..', '..')))
-from pypi_org.bin.load_data 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
@@ -40,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')
@@ -75,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')
@@ -101,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()
@@ -135,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:
@@ -172,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:
@@ -185,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
@@ -216,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
@@ -234,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
@@ -284,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()
@@ -351,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 0c0941eb..d944c142 100644
--- a/app/ch11_migrations/final/pypi_org/infrastructure/view_modifiers.py
+++ b/app/ch11_migrations/final/pypi_org/infrastructure/view_modifiers.py
@@ -26,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)
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 ba62ff7d..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,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__), '..', '..')))
from pypi_org.infrastructure.num_convert import try_int
import pypi_org.data.db_session as db_session
@@ -40,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')
@@ -75,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')
@@ -101,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()
@@ -135,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:
@@ -172,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:
@@ -185,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
@@ -216,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
@@ -234,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
@@ -284,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()
@@ -351,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
index bf88a6bc..4e1d8879 100644
--- a/app/ch11_migrations/starter/pypi_org/infrastructure/num_convert.py
+++ b/app/ch11_migrations/starter/pypi_org/infrastructure/num_convert.py
@@ -1,5 +1,8 @@
-def try_int(text) -> int:
+from typing import Optional
+
+
+def try_int(text) -> Optional[int]:
try:
return int(text)
except:
- return 0
+ 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 0978163e..d944c142 100644
--- a/app/ch11_migrations/starter/pypi_org/infrastructure/view_modifiers.py
+++ b/app/ch11_migrations/starter/pypi_org/infrastructure/view_modifiers.py
@@ -26,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)
@@ -42,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 298cdef1..7a996036 100644
--- a/app/ch12-forms/final/.idea/dataSources.local.xml
+++ b/app/ch12-forms/final/.idea/dataSources.local.xml
@@ -6,7 +6,7 @@
"
- false
+ no-auth
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 75%
rename from app/ch12-forms/final/.idea/flask-html-forms.iml
rename to app/ch12-forms/final/.idea/flask ch12-forms - final.iml
index 776f1339..80a07251 100644
--- a/app/ch12-forms/final/.idea/flask-html-forms.iml
+++ b/app/ch12-forms/final/.idea/flask ch12-forms - final.iml
@@ -4,7 +4,7 @@
-
+
@@ -15,7 +15,4 @@
-
-
-
\ 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 2227f9db..0fbd8016 100644
--- a/app/ch12-forms/final/pypi_org/app.py
+++ b/app/ch12-forms/final/pypi_org/app.py
@@ -21,10 +21,7 @@ def configure():
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/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 ba62ff7d..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,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__), '..', '..')))
from pypi_org.infrastructure.num_convert import try_int
import pypi_org.data.db_session as db_session
@@ -40,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')
@@ -75,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')
@@ -101,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()
@@ -135,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:
@@ -172,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:
@@ -185,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
@@ -216,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
@@ -234,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
@@ -284,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()
@@ -351,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
index bf88a6bc..705fbd60 100644
--- a/app/ch12-forms/final/pypi_org/infrastructure/num_convert.py
+++ b/app/ch12-forms/final/pypi_org/infrastructure/num_convert.py
@@ -1,4 +1,7 @@
-def try_int(text) -> int:
+from typing import Optional
+
+
+def try_int(text) -> Optional[int]:
try:
return int(text)
except:
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 d7ab10de..d44ae574 100644
--- a/app/ch12-forms/final/pypi_org/infrastructure/request_dict.py
+++ b/app/ch12-forms/final/pypi_org/infrastructure/request_dict.py
@@ -29,7 +29,7 @@ def create(default_val=None, **route_args) -> RequestDictionary:
**args, # The key/value pairs in the URL query string
**request.headers, # Header values
**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.
+ **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 0978163e..d944c142 100644
--- a/app/ch12-forms/final/pypi_org/infrastructure/view_modifiers.py
+++ b/app/ch12-forms/final/pypi_org/infrastructure/view_modifiers.py
@@ -26,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)
@@ -42,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 ba62ff7d..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,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__), '..', '..')))
from pypi_org.infrastructure.num_convert import try_int
import pypi_org.data.db_session as db_session
@@ -40,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')
@@ -75,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')
@@ -101,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()
@@ -135,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:
@@ -172,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:
@@ -185,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
@@ -216,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
@@ -234,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
@@ -284,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()
@@ -351,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
index bf88a6bc..4e1d8879 100644
--- a/app/ch12-forms/starter/pypi_org/infrastructure/num_convert.py
+++ b/app/ch12-forms/starter/pypi_org/infrastructure/num_convert.py
@@ -1,5 +1,8 @@
-def try_int(text) -> int:
+from typing import Optional
+
+
+def try_int(text) -> Optional[int]:
try:
return int(text)
except:
- return 0
+ 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 0978163e..d944c142 100644
--- a/app/ch12-forms/starter/pypi_org/infrastructure/view_modifiers.py
+++ b/app/ch12-forms/starter/pypi_org/infrastructure/view_modifiers.py
@@ -26,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)
@@ -42,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 277a48f8..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,10 +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.bin.load_data import try_int
+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
@@ -40,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')
@@ -75,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')
@@ -101,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()
@@ -135,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:
@@ -172,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:
@@ -185,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
@@ -216,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
@@ -234,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
@@ -284,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()
@@ -351,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 a5b3efd9..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,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/ch13-validation/final/pypi_org/infrastructure/num_convert.py b/app/ch13-validation/final/pypi_org/infrastructure/num_convert.py
index bf88a6bc..4e1d8879 100644
--- a/app/ch13-validation/final/pypi_org/infrastructure/num_convert.py
+++ b/app/ch13-validation/final/pypi_org/infrastructure/num_convert.py
@@ -1,5 +1,8 @@
-def try_int(text) -> int:
+from typing import Optional
+
+
+def try_int(text) -> Optional[int]:
try:
return int(text)
except:
- return 0
+ 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 d7ab10de..d44ae574 100644
--- a/app/ch13-validation/final/pypi_org/infrastructure/request_dict.py
+++ b/app/ch13-validation/final/pypi_org/infrastructure/request_dict.py
@@ -29,7 +29,7 @@ def create(default_val=None, **route_args) -> RequestDictionary:
**args, # The key/value pairs in the URL query string
**request.headers, # Header values
**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.
+ **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 0978163e..d944c142 100644
--- a/app/ch13-validation/final/pypi_org/infrastructure/view_modifiers.py
+++ b/app/ch13-validation/final/pypi_org/infrastructure/view_modifiers.py
@@ -26,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)
@@ -42,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 a5b3efd9..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,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/ch13-validation/starter/pypi_org/infrastructure/num_convert.py b/app/ch13-validation/starter/pypi_org/infrastructure/num_convert.py
index bf88a6bc..4e1d8879 100644
--- a/app/ch13-validation/starter/pypi_org/infrastructure/num_convert.py
+++ b/app/ch13-validation/starter/pypi_org/infrastructure/num_convert.py
@@ -1,5 +1,8 @@
-def try_int(text) -> int:
+from typing import Optional
+
+
+def try_int(text) -> Optional[int]:
try:
return int(text)
except:
- return 0
+ 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 d7ab10de..d44ae574 100644
--- a/app/ch13-validation/starter/pypi_org/infrastructure/request_dict.py
+++ b/app/ch13-validation/starter/pypi_org/infrastructure/request_dict.py
@@ -29,7 +29,7 @@ def create(default_val=None, **route_args) -> RequestDictionary:
**args, # The key/value pairs in the URL query string
**request.headers, # Header values
**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.
+ **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 0978163e..d944c142 100644
--- a/app/ch13-validation/starter/pypi_org/infrastructure/view_modifiers.py
+++ b/app/ch13-validation/starter/pypi_org/infrastructure/view_modifiers.py
@@ -26,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)
@@ -42,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 a5b3efd9..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,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=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
index bf88a6bc..4e1d8879 100644
--- a/app/ch14_testing/final/pypi_org/infrastructure/num_convert.py
+++ b/app/ch14_testing/final/pypi_org/infrastructure/num_convert.py
@@ -1,5 +1,8 @@
-def try_int(text) -> int:
+from typing import Optional
+
+
+def try_int(text) -> Optional[int]:
try:
return int(text)
except:
- return 0
+ 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 d7ab10de..d44ae574 100644
--- a/app/ch14_testing/final/pypi_org/infrastructure/request_dict.py
+++ b/app/ch14_testing/final/pypi_org/infrastructure/request_dict.py
@@ -29,7 +29,7 @@ def create(default_val=None, **route_args) -> RequestDictionary:
**args, # The key/value pairs in the URL query string
**request.headers, # Header values
**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.
+ **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 0978163e..d944c142 100644
--- a/app/ch14_testing/final/pypi_org/infrastructure/view_modifiers.py
+++ b/app/ch14_testing/final/pypi_org/infrastructure/view_modifiers.py
@@ -26,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)
@@ -42,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 a5b3efd9..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,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/ch14_testing/starter/pypi_org/infrastructure/num_convert.py b/app/ch14_testing/starter/pypi_org/infrastructure/num_convert.py
index bf88a6bc..4e1d8879 100644
--- a/app/ch14_testing/starter/pypi_org/infrastructure/num_convert.py
+++ b/app/ch14_testing/starter/pypi_org/infrastructure/num_convert.py
@@ -1,5 +1,8 @@
-def try_int(text) -> int:
+from typing import Optional
+
+
+def try_int(text) -> Optional[int]:
try:
return int(text)
except:
- return 0
+ 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 d7ab10de..d44ae574 100644
--- a/app/ch14_testing/starter/pypi_org/infrastructure/request_dict.py
+++ b/app/ch14_testing/starter/pypi_org/infrastructure/request_dict.py
@@ -29,7 +29,7 @@ def create(default_val=None, **route_args) -> RequestDictionary:
**args, # The key/value pairs in the URL query string
**request.headers, # Header values
**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.
+ **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 0978163e..d944c142 100644
--- a/app/ch14_testing/starter/pypi_org/infrastructure/view_modifiers.py
+++ b/app/ch14_testing/starter/pypi_org/infrastructure/view_modifiers.py
@@ -26,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)
@@ -42,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 a5b3efd9..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,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/ch15_deploy/final/pypi_org/infrastructure/num_convert.py b/app/ch15_deploy/final/pypi_org/infrastructure/num_convert.py
index bf88a6bc..4e1d8879 100644
--- a/app/ch15_deploy/final/pypi_org/infrastructure/num_convert.py
+++ b/app/ch15_deploy/final/pypi_org/infrastructure/num_convert.py
@@ -1,5 +1,8 @@
-def try_int(text) -> int:
+from typing import Optional
+
+
+def try_int(text) -> Optional[int]:
try:
return int(text)
except:
- return 0
+ 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 d7ab10de..d44ae574 100644
--- a/app/ch15_deploy/final/pypi_org/infrastructure/request_dict.py
+++ b/app/ch15_deploy/final/pypi_org/infrastructure/request_dict.py
@@ -29,7 +29,7 @@ def create(default_val=None, **route_args) -> RequestDictionary:
**args, # The key/value pairs in the URL query string
**request.headers, # Header values
**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.
+ **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 0978163e..d944c142 100644
--- a/app/ch15_deploy/final/pypi_org/infrastructure/view_modifiers.py
+++ b/app/ch15_deploy/final/pypi_org/infrastructure/view_modifiers.py
@@ -26,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)
@@ -42,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 e8ba36b0..19fc8c45 100644
--- a/app/ch15_deploy/final/requirements.txt
+++ b/app/ch15_deploy/final/requirements.txt
@@ -1,7 +1,42 @@
-flask
-werkzeug
-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 a5b3efd9..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,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/ch15_deploy/starter/pypi_org/infrastructure/num_convert.py b/app/ch15_deploy/starter/pypi_org/infrastructure/num_convert.py
index bf88a6bc..4e1d8879 100644
--- a/app/ch15_deploy/starter/pypi_org/infrastructure/num_convert.py
+++ b/app/ch15_deploy/starter/pypi_org/infrastructure/num_convert.py
@@ -1,5 +1,8 @@
-def try_int(text) -> int:
+from typing import Optional
+
+
+def try_int(text) -> Optional[int]:
try:
return int(text)
except:
- return 0
+ 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 d7ab10de..d44ae574 100644
--- a/app/ch15_deploy/starter/pypi_org/infrastructure/request_dict.py
+++ b/app/ch15_deploy/starter/pypi_org/infrastructure/request_dict.py
@@ -29,7 +29,7 @@ def create(default_val=None, **route_args) -> RequestDictionary:
**args, # The key/value pairs in the URL query string
**request.headers, # Header values
**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.
+ **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 0978163e..d944c142 100644
--- a/app/ch15_deploy/starter/pypi_org/infrastructure/view_modifiers.py
+++ b/app/ch15_deploy/starter/pypi_org/infrastructure/view_modifiers.py
@@ -26,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)
@@ -42,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 a5b3efd9..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,19 +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.infrastructure.num_convert 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:
@@ -21,11 +21,7 @@ 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]:
+def get_user_id_via_auth_cookie(request: Request) -> Optional[bson.ObjectId]:
if auth_cookie_name not in request.cookies:
return None
@@ -38,10 +34,10 @@ 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)
+ 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
index bf88a6bc..007e3ff8 100644
--- a/app/ch16_mongodb/final/pypi_org/infrastructure/num_convert.py
+++ b/app/ch16_mongodb/final/pypi_org/infrastructure/num_convert.py
@@ -1,5 +1,17 @@
-def try_int(text) -> int:
+from typing import Optional
+
+import bson
+
+
+def try_int(text) -> Optional[int]:
try:
return int(text)
except:
- return 0
+ 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 d7ab10de..d44ae574 100644
--- a/app/ch16_mongodb/final/pypi_org/infrastructure/request_dict.py
+++ b/app/ch16_mongodb/final/pypi_org/infrastructure/request_dict.py
@@ -29,7 +29,7 @@ def create(default_val=None, **route_args) -> RequestDictionary:
**args, # The key/value pairs in the URL query string
**request.headers, # Header values
**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.
+ **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 0978163e..d944c142 100644
--- a/app/ch16_mongodb/final/pypi_org/infrastructure/view_modifiers.py
+++ b/app/ch16_mongodb/final/pypi_org/infrastructure/view_modifiers.py
@@ -26,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)
@@ -42,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 a5b3efd9..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,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/ch16_mongodb/starter/pypi_org/infrastructure/num_convert.py b/app/ch16_mongodb/starter/pypi_org/infrastructure/num_convert.py
index bf88a6bc..4e1d8879 100644
--- a/app/ch16_mongodb/starter/pypi_org/infrastructure/num_convert.py
+++ b/app/ch16_mongodb/starter/pypi_org/infrastructure/num_convert.py
@@ -1,5 +1,8 @@
-def try_int(text) -> int:
+from typing import Optional
+
+
+def try_int(text) -> Optional[int]:
try:
return int(text)
except:
- return 0
+ 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 d7ab10de..d44ae574 100644
--- a/app/ch16_mongodb/starter/pypi_org/infrastructure/request_dict.py
+++ b/app/ch16_mongodb/starter/pypi_org/infrastructure/request_dict.py
@@ -29,7 +29,7 @@ def create(default_val=None, **route_args) -> RequestDictionary:
**args, # The key/value pairs in the URL query string
**request.headers, # Header values
**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.
+ **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 0978163e..d944c142 100644
--- a/app/ch16_mongodb/starter/pypi_org/infrastructure/view_modifiers.py
+++ b/app/ch16_mongodb/starter/pypi_org/infrastructure/view_modifiers.py
@@ -26,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)
@@ -42,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