Coverage for middle_layer/common/application_layer/rest_api/server.py: 100.00%
76 statements
« prev ^ index » next coverage.py v7.10.5, created at 2026-04-13 06:13 +0000
« prev ^ index » next coverage.py v7.10.5, created at 2026-04-13 06:13 +0000
1# Copyright 2024 Associated Universities, Inc.
2#
3# This file is part of Telescope Time Allocation Tools (TTAT).
4#
5# TTAT is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# any later version.
9#
10# TTAT is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with TTAT. If not, see <https://www.gnu.org/licenses/>.
17#
18import atexit
19import logging
20from datetime import timezone
21from json import JSONEncoder
23# intellij doesn't like the next line, but docker does, so we need to work that out
24import sentry_sdk
25from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
26from apscheduler.schedulers.background import BackgroundScheduler
27from paste.translogger import TransLogger
28from pyramid.config import Configurator
29from pyramid.events import NewRequest
30from pyramid.renderers import JSONP
31from pyramid.request import Request
32from pyramid.response import Response
33from sentry_sdk.integrations.pyramid import PyramidIntegration
34from zope.sqlalchemy import register
36from allocate.application_layer.rest_api import views as allocate_views
37from closeout.application_layer.rest_api import views as closeout_views
38from common.application_layer.orm_repositories.initialize_persistence import (
39 get_database_url,
40 initialize_db_session_factory,
41)
42from common.application_layer.orm_repositories.orm_repository import ORMRepository
43from common.application_layer.rest_api import get_entity_404
44from common.application_layer.rest_api import views as common_views
45from common.utils.gitlab_secret_loader import SENTRY_DSN
46from misc.application_layer.rest_api import views as misc_views
47from propose.application_layer.rest_api import views as propose_views
48from review.application_layer.rest_api import views as review_views
49from solicit.application_layer.rest_api import views as solicit_views
50from testdata.application_layer.rest_api import views as testdata_views
52encoder = JSONEncoder()
55# Copied from here: https://stackoverflow.com/questions/21107057/pyramid-cors-for-ajax-requests
56def add_cors_headers_response_callback(event: NewRequest):
57 """
58 Event handler that adds CORS HTTP headers to responses from this server; allows external servers
59 (the front end) to send requests and not have them bounce off
61 More information about CORS: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
63 :param event: Server event
64 """
66 def cors_headers(request: Request, response: Response):
67 """
68 Callback function that adds CORS headers to a response
70 :param request: the request
71 :param response: Server response
72 """
73 response.headers.update(
74 {
75 "Access-Control-Allow-Origin": "*",
76 "Access-Control-Allow-Methods": "POST,GET,DELETE,PUT,OPTIONS",
77 "Access-Control-Allow-Headers": "Origin, Content-Type, Accept, Authorization",
78 "Access-Control-Allow-Credentials": "true",
79 "Access-Control-Max-Age": "1728000",
80 }
81 )
83 event.request.add_response_callback(cors_headers)
86def get_tm_session(session_factory, transaction_manager):
87 """
88 Enable Zope's transaction manager on our session
89 :param session_factory:
90 :param transaction_manager:
91 :return:
92 """
93 dbsession = session_factory(future=True)
94 register(dbsession, transaction_manager=transaction_manager)
95 return dbsession
98# ---------------------------------------------------------
99#
100# M A I N E N T R Y P O I N T
101#
102# ---------------------------------------------------------
105def main(global_config, **settings):
107 # Initialize Sentry if DSN is provided
108 if SENTRY_DSN:
109 sentry_sdk.init(
110 dsn=SENTRY_DSN,
111 send_default_pii=settings.get("sentry.send_default_pii", "false").lower() == "true",
112 integrations=[PyramidIntegration()],
113 )
115 settings["tm.manager_hook"] = "pyramid_tm.explicit_manager"
116 with Configurator(settings=settings) as config:
117 config.add_subscriber(add_cors_headers_response_callback, NewRequest)
118 config.add_renderer("jsonp", JSONP(param_name="callback"))
120 # include the security policy - see jwtauth package
121 config.include("common.application_layer.rest_api.jwtauth")
122 # now load the security policy - passing in the JWT secret
123 # TODO: that secret should probably come from GitLab
124 config.set_jwt_security_policy("ttat_secret", audience="ttat")
126 # Add repository to request
127 session_factory = initialize_db_session_factory()
128 config.set_session_factory(session_factory)
129 config.registry["dbsession_factory"] = session_factory
131 def create_repo(request):
132 session = get_tm_session(session_factory, request.tm)
133 return ORMRepository(session)
135 config.add_request_method(create_repo, "repo", reify=True)
136 config.add_request_method(get_entity_404, name="lookup")
138 scheduler = BackgroundScheduler(
139 # Need the Scheduler to have a separate engine since it'll be running in a different process
140 # Source: https://docs.sqlalchemy.org/en/14/core/pooling.html#pooling-multiprocessing
141 jobstores={"default": SQLAlchemyJobStore(url=get_database_url())},
142 timezone=timezone.utc,
143 job_defaults={"misfire_grace_time": None},
144 )
145 # Add scheduler to request
146 config.add_request_method(
147 lambda _: scheduler,
148 "scheduler",
149 reify=True,
150 )
151 atexit.register(scheduler.shutdown)
152 scheduler.start()
153 logging.getLogger("apscheduler").setLevel(logging.DEBUG)
155 # Include routes from routes files
156 config.include("common.application_layer.rest_api.routes")
157 config.include("solicit.application_layer.rest_api.routes")
158 config.include("propose.application_layer.rest_api.routes")
159 config.include("misc.application_layer.rest_api.routes")
160 config.include("review.application_layer.rest_api.routes")
161 config.include("testdata.application_layer.rest_api.routes")
162 config.include("allocate.application_layer.rest_api.routes")
163 config.include("closeout.application_layer.rest_api.routes")
165 # Include views from views files
166 config.scan(common_views)
167 config.scan(propose_views)
168 config.scan(solicit_views)
169 config.scan(misc_views)
170 config.scan(review_views)
171 config.scan(testdata_views)
172 config.scan(allocate_views)
173 config.scan(closeout_views)
175 app = config.make_wsgi_app()
176 return TransLogger(app)