Coverage for webapp/app.py: 96%

79 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-25 22:09 +0000

1""" 

2A Flask application for snapcraft.io. 

3 

4The web frontend for the snap store. 

5""" 

6 

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 

10 

11import sentry_sdk 

12from flask import send_from_directory 

13 

14try: 

15 from gevent import GreenletExit 

16except ImportError: 

17 GreenletExit = None 

18 

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 

46 

47 

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) 

56 

57 sentry_sdk.init(dsn=SENTRY_DSN, ignore_errors=ignore_errors) 

58 

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 

70 

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 ) 

84 

85 app.add_url_rule( 

86 "/icons/<icon_name>.svg", 

87 "ds-icons", 

88 serve_ds_icon, 

89 ) 

90 

91 init_extensions(app) 

92 set_handlers(app) 

93 

94 # Serve any page as Markdown at /page.md and llms.txt 

95 store_llm.init_app(app) 

96 

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") 

119 

120 return app 

121 

122 

123def init_extensions(app: FlaskBase): 

124 vite.init_app(app) 

125 

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")