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

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 

22 

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 

35 

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 

51 

52encoder = JSONEncoder() 

53 

54 

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 

60 

61 More information about CORS: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS 

62 

63 :param event: Server event 

64 """ 

65 

66 def cors_headers(request: Request, response: Response): 

67 """ 

68 Callback function that adds CORS headers to a response 

69 

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 ) 

82 

83 event.request.add_response_callback(cors_headers) 

84 

85 

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 

96 

97 

98# --------------------------------------------------------- 

99# 

100# M A I N E N T R Y P O I N T 

101# 

102# --------------------------------------------------------- 

103 

104 

105def main(global_config, **settings): 

106 

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 ) 

114 

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

119 

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

125 

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 

130 

131 def create_repo(request): 

132 session = get_tm_session(session_factory, request.tm) 

133 return ORMRepository(session) 

134 

135 config.add_request_method(create_repo, "repo", reify=True) 

136 config.add_request_method(get_entity_404, name="lookup") 

137 

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) 

154 

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

164 

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) 

174 

175 app = config.make_wsgi_app() 

176 return TransLogger(app)