app.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. """
  2. Starting point of the Flask application.
  3. """
  4. from __future__ import annotations
  5. import time
  6. from copy import copy
  7. import os
  8. from pathlib import Path
  9. from datetime import date
  10. from flask import Flask, g, request
  11. from flask.cli import load_dotenv
  12. from flask_mail import Mail
  13. from flask_sslify import SSLify
  14. from flask_json import FlaskJSON
  15. from flask_cors import CORS
  16. from redis import Redis
  17. from rq import Queue
  18. from flexmeasures.data.services.job_cache import JobCache
  19. def create( # noqa C901
  20. env: str | None = None,
  21. path_to_config: str | None = None,
  22. plugins: list[str] | None = None,
  23. ) -> Flask:
  24. """
  25. Create a Flask app and configure it.
  26. Set the environment by setting FLEXMEASURES_ENV as environment variable (also possible in .env).
  27. Or, overwrite any FLEXMEASURES_ENV setting by passing an env in directly (useful for testing for instance).
  28. A path to a config file can be passed in (otherwise a config file will be searched in the home or instance directories).
  29. Also, a list of plugins can be set. Usually this works as a config setting, but this is useful for automated testing.
  30. """
  31. from flexmeasures.utils import config_defaults
  32. from flexmeasures.utils.config_utils import read_config, configure_logging
  33. from flexmeasures.utils.app_utils import set_secret_key, init_sentry
  34. from flexmeasures.utils.error_utils import add_basic_error_handlers
  35. # Create app
  36. configure_logging() # do this first, see https://flask.palletsprojects.com/en/2.0.x/logging
  37. # we're loading dotenv files manually & early (can do Flask.run(load_dotenv=False)),
  38. # as we need to know the ENV now (for it to be recognised by Flask()).
  39. load_dotenv()
  40. app = Flask("flexmeasures")
  41. if env is not None: # overwrite
  42. app.config["FLEXMEASURES_ENV"] = env
  43. if app.config.get("FLEXMEASURES_ENV") == "testing":
  44. app.testing = True
  45. if app.config.get("FLEXMEASURES_ENV") == "development":
  46. app.debug = config_defaults.DevelopmentConfig.DEBUG
  47. # App configuration
  48. read_config(app, custom_path_to_config=path_to_config)
  49. if plugins:
  50. app.config["FLEXMEASURES_PLUGINS"] += plugins
  51. add_basic_error_handlers(app)
  52. if (
  53. app.config.get("FLEXMEASURES_ENV") not in ("development", "documentation")
  54. and not app.testing
  55. ):
  56. init_sentry(app)
  57. app.mail = Mail(app)
  58. FlaskJSON(app)
  59. CORS(app)
  60. # configure Redis (for redis queue)
  61. if app.testing:
  62. from fakeredis import FakeStrictRedis
  63. redis_conn = FakeStrictRedis(
  64. host="redis", port="1234"
  65. ) # dummy connection details
  66. else:
  67. redis_conn = Redis(
  68. app.config["FLEXMEASURES_REDIS_URL"],
  69. port=app.config["FLEXMEASURES_REDIS_PORT"],
  70. db=app.config["FLEXMEASURES_REDIS_DB_NR"],
  71. password=app.config["FLEXMEASURES_REDIS_PASSWORD"],
  72. )
  73. """ FWIW, you could use redislite like this (not on non-recent os.name=="nt" systems or PA, sadly):
  74. from redislite import Redis
  75. redis_conn = Redis("MY-DB-NAME", unix_socket_path="/tmp/my-redis.socket",
  76. )
  77. """
  78. app.redis_connection = redis_conn
  79. app.queues = dict(
  80. forecasting=Queue(connection=redis_conn, name="forecasting"),
  81. scheduling=Queue(connection=redis_conn, name="scheduling"),
  82. # reporting=Queue(connection=redis_conn, name="reporting"),
  83. # labelling=Queue(connection=redis_conn, name="labelling"),
  84. # alerting=Queue(connection=redis_conn, name="alerting"),
  85. )
  86. app.job_cache = JobCache(app.redis_connection)
  87. # Some basic security measures
  88. set_secret_key(app)
  89. if app.config.get("SECURITY_PASSWORD_SALT", None) is None:
  90. app.config["SECURITY_PASSWORD_SALT"] = app.config["SECRET_KEY"]
  91. if app.config.get("FLEXMEASURES_FORCE_HTTPS", False):
  92. SSLify(app)
  93. # Prepare profiling, if needed
  94. if app.config.get("FLEXMEASURES_PROFILE_REQUESTS", False):
  95. Path("profile_reports").mkdir(parents=True, exist_ok=True)
  96. try:
  97. import pyinstrument # noqa F401
  98. except ImportError:
  99. app.logger.warning(
  100. "FLEXMEASURES_PROFILE_REQUESTS is True, but pyinstrument not installed ― I cannot produce profiling reports for requests."
  101. )
  102. # Register database and models, including user auth security handlers
  103. from flexmeasures.data import register_at as register_db_at
  104. register_db_at(app)
  105. # Register Reporters and Schedulers
  106. from flexmeasures.utils.coding_utils import get_classes_module
  107. from flexmeasures.data.models import reporting, planning
  108. reporters = get_classes_module("flexmeasures.data.models", reporting.Reporter)
  109. schedulers = get_classes_module("flexmeasures.data.models", planning.Scheduler)
  110. app.data_generators = dict()
  111. app.data_generators["reporter"] = copy(
  112. reporters
  113. ) # use copy to avoid mutating app.reporters
  114. app.data_generators["scheduler"] = schedulers
  115. # add auth policy
  116. from flexmeasures.auth import register_at as register_auth_at
  117. register_auth_at(app)
  118. # This needs to happen here because for unknown reasons, Security(app)
  119. # and FlaskJSON() will set this to False on their own
  120. if app.config.get("FLEXMEASURES_JSON_COMPACT", False) in (
  121. True,
  122. "True",
  123. "true",
  124. "1",
  125. "yes",
  126. ):
  127. app.json.compact = True
  128. else:
  129. app.json.compact = False
  130. # Register the CLI
  131. from flexmeasures.cli import register_at as register_cli_at
  132. register_cli_at(app)
  133. # Register the API
  134. from flexmeasures.api import register_at as register_api_at
  135. register_api_at(app)
  136. # Register plugins
  137. # If plugins register routes, they'll have precedence over standard UI
  138. # routes (first registration wins). However, we want to control "/" separately.
  139. from flexmeasures.utils.app_utils import root_dispatcher
  140. from flexmeasures.utils.plugin_utils import register_plugins
  141. app.add_url_rule("/", view_func=root_dispatcher)
  142. register_plugins(app)
  143. # Register the UI
  144. from flexmeasures.ui import register_at as register_ui_at
  145. register_ui_at(app)
  146. # Global template variables for both our own templates and external templates
  147. @app.context_processor
  148. def set_global_template_variables():
  149. return {"queue_names": app.queues.keys()}
  150. # Profile endpoints (if needed, e.g. during development)
  151. @app.before_request
  152. def before_request():
  153. if app.config.get("FLEXMEASURES_PROFILE_REQUESTS", False):
  154. g.start = time.time()
  155. try:
  156. import pyinstrument # noqa F401
  157. g.profiler = pyinstrument.Profiler(async_mode="disabled")
  158. g.profiler.start()
  159. except ImportError:
  160. pass
  161. @app.teardown_request
  162. def teardown_request(exception=None):
  163. if app.config.get("FLEXMEASURES_PROFILE_REQUESTS", False):
  164. diff = time.time() - g.start
  165. if all([kw not in request.url for kw in ["/static", "favicon.ico"]]):
  166. app.logger.info(
  167. f"[PROFILE] {str(round(diff, 2)).rjust(6)} seconds to serve {request.url}."
  168. )
  169. if not hasattr(g, "profiler"):
  170. return app
  171. g.profiler.stop()
  172. output_html = g.profiler.output_html()
  173. endpoint = request.endpoint
  174. if endpoint is None:
  175. endpoint = "unknown"
  176. today = date.today()
  177. profile_filename = f"pyinstrument_{endpoint}.html"
  178. profile_output_path = Path(
  179. "profile_reports", today.strftime("%Y-%m-%d")
  180. )
  181. profile_output_path.mkdir(parents=True, exist_ok=True)
  182. with open(
  183. os.path.join(profile_output_path, profile_filename), "w+"
  184. ) as f:
  185. f.write(output_html)
  186. return app