Coverage for webapp/app.py: 96%
79 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-25 22:09 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-25 22:09 +0000
1"""
2A Flask application for snapcraft.io.
4The web frontend for the snap store.
5"""
7# We import the config module before anything else to make sure env vars are
8# loaded properly and the FLASK_* prefix is stripped before they are parsed
9import webapp.config # noqa: F401
11import sentry_sdk
12from flask import send_from_directory
14try:
15 from gevent import GreenletExit
16except ImportError:
17 GreenletExit = None
19from canonicalwebteam.flask_base.app import FlaskBase
20from webapp.blog.views import init_blog
21from webapp.docs.views import init_docs
22from webapp.extensions import csrf, vite
23from webapp.handlers import set_handlers
24from webapp.login.views import login
25from webapp.llms import store_llm
26from webapp.login.oauth_views import oauth
27from webapp.publisher.snaps.views import publisher_snaps
28from webapp.publisher.github.views import publisher_github
29from webapp.admin.views import admin
30from webapp.publisher.views import account
31from webapp.snapcraft.views import snapcraft_blueprint
32from webapp.store.views import store_blueprint
33from webapp.tutorials.views import init_tutorials
34from webapp.packages.store_packages import store_packages
35from webapp.endpoints.views import endpoints
36from webapp.endpoints.signing_keys import signing_keys
37from webapp.endpoints.models import models
38from webapp.endpoints.members import members
39from webapp.endpoints.snaps import snaps
40from webapp.endpoints.snap_search import snap_search
41from webapp.endpoints.validation_sets import validation_sets
42from webapp.endpoints.invites import invites
43from webapp.endpoints.settings import settings
44from webapp.feeds.feeds import feeds
45from webapp.config import SENTRY_DSN
48def create_app(testing=False):
49 ignore_errors = []
50 if GreenletExit is not None:
51 # GreenletExit is raised by gevent when a gunicorn worker is
52 # stopped or recycled (e.g. during a deploy) while a request
53 # is in flight. It is expected shutdown behaviour, not an app
54 # error, so we do not report it to Sentry.
55 ignore_errors.append(GreenletExit)
57 sentry_sdk.init(dsn=SENTRY_DSN, ignore_errors=ignore_errors)
59 app = FlaskBase(
60 __name__,
61 "snapcraft.io",
62 favicon_url="https://assets.ubuntu.com/v1/d4ca039f-favicon_16px.png",
63 template_404="404.html",
64 template_folder="../templates",
65 static_folder="../static",
66 )
67 app.config.from_object("webapp.config")
68 app.name = "snapcraft"
69 app.testing = testing
71 # This is required because Pragma icons are served from `/icons` at the
72 # root of the project, which is a problem for snapcraft.io as that is
73 # the URL pattern we use for snap names. To resolve this, we are
74 # serving any requests to `/icons/<ICON_NAME>.svg` from static icons,
75 # which leaves the possibility for a snap called "icons" and its
76 # subsequent publisher pages.
77 #
78 # DS icons are exposed through the tracked `static/icons` symlink.
79 def serve_ds_icon(icon_name):
80 return send_from_directory(
81 f"{app.static_folder}/icons",
82 f"{icon_name}.svg",
83 )
85 app.add_url_rule(
86 "/icons/<icon_name>.svg",
87 "ds-icons",
88 serve_ds_icon,
89 )
91 init_extensions(app)
92 set_handlers(app)
94 # Serve any page as Markdown at /page.md and llms.txt
95 store_llm.init_app(app)
97 app.register_blueprint(snapcraft_blueprint())
98 app.register_blueprint(store_packages)
99 app.register_blueprint(login)
100 app.register_blueprint(oauth)
101 app.register_blueprint(store_blueprint())
102 app.register_blueprint(account, url_prefix="/account")
103 app.register_blueprint(publisher_snaps)
104 app.register_blueprint(publisher_github)
105 app.register_blueprint(admin)
106 app.register_blueprint(endpoints)
107 app.register_blueprint(signing_keys)
108 app.register_blueprint(models)
109 app.register_blueprint(members)
110 app.register_blueprint(snaps)
111 app.register_blueprint(snap_search)
112 app.register_blueprint(validation_sets)
113 app.register_blueprint(invites)
114 app.register_blueprint(settings)
115 app.register_blueprint(feeds)
116 init_docs(app, "/docs")
117 init_blog(app, "/blog")
118 init_tutorials(app, "/tutorials")
120 return app
123def init_extensions(app: FlaskBase):
124 vite.init_app(app)
126 if not app.testing:
127 csrf.init_app(app)
128 else:
129 # add a helper for injecting a mock CSRF token
130 @app.context_processor
131 def inject_csrf_token():
132 return dict(csrf_token=lambda: "mocked_csrf_token")