def get_level(student_id: str) -> int: with pg_conn_context() as conn, conn.cursor() as cursor: select_query = "SELECT level FROM privacy_settings WHERE student_id=%s" cursor.execute(select_query, (student_id, )) result = cursor.fetchone() return result[0] if result is not None else get_config( ).DEFAULT_PRIVACY_LEVEL
def _get_datetime(week: int, day: int, time: Tuple[int, int], semester: Tuple[int, int, int]) -> datetime: """ 根据学期、周次、时间,生成 `datetime` 类型的时间 :param week: 周次 :param day: 星期 :param time: 时间tuple(时,分) :param semester: 学期 :return: datetime 类型的时间 """ config = get_config() tz = pytz.timezone("Asia/Shanghai") dt = datetime(*(config.AVAILABLE_SEMESTERS[semester]['start'] + time), tzinfo=tz) # noqa: T484 dt += timedelta(days=(week - 1) * 7 + day) # 调整到当前周 if 'adjustments' in config.AVAILABLE_SEMESTERS[semester]: ymd = (dt.year, dt.month, dt.day) adjustments = config.AVAILABLE_SEMESTERS[semester]['adjustments'] if ymd in adjustments: if adjustments[ymd]['to']: # 调课 dt = dt.replace(year=adjustments[ymd]['to'][0], month=adjustments[ymd]['to'][1], day=adjustments[ymd]['to'][2]) else: # 冲掉的课年份设置为1984,返回之后被抹去 dt = dt.replace(year=1984) return dt
def decode_jwt_payload(token: str) -> Dict: """验证JWT Token并解出payload 如果payload被修改,抛出jwt.exceptions.InvalidSignatureError。如果签名被修改,抛出jwt.exceptions.DecodeError """ from everyclass.server.utils.config import get_config config = get_config() return jwt.decode(token, config.JWT_PUBLIC_KEY, algorithms=['RS256'])
def issue_token(user_identifier: str) -> str: """签发指定用户名的JWT token""" from everyclass.server.utils.config import get_config config = get_config() payload = {"username": user_identifier} token = jwt.encode(payload, config.JWT_PRIVATE_KEY, algorithm='RS256') return token.decode('utf8')
def plugin_available(plugin_name: str) -> bool: """ check if a plugin (Sentry, apm, logstash) is available in the current environment. :return True if available else False """ from everyclass.server.utils.config import get_config config = get_config() mode = os.environ.get("MODE", None) if mode: return mode.lower() in getattr( config, "{}_AVAILABLE_IN".format(plugin_name).upper()) else: raise EnvironmentError("MODE not in environment variables")
def enter_maintenance(): config = get_config() auth = request.authorization if auth \ and auth.username in config.MAINTENANCE_CREDENTIALS \ and config.MAINTENANCE_CREDENTIALS[auth.username] == auth.password: open(config.MAINTENANCE_FILE, "w+").close() # maintenance file open(os.path.join(os.getcwd(), 'reload'), "w+").close() # uwsgi reload return 'success' else: return Response( 'Could not verify your access level for that URL.\n' 'You have to login with proper credentials', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
def encrypt(resource_type: str, data: str, encryption_key: str = None) -> Text: """ 加密资源标识符 :param resource_type: student、teacher、klass、room :param data: 资源标识符 :param encryption_key: 加密使用的 key :return: 加密后的资源标识符 """ if resource_type not in (RTYPE_STUDENT, RTYPE_TEACHER, RTYPE_CLASS, RTYPE_ROOM, RTYPE_PEOPLE): raise ValueError("resource_type not valid") if not encryption_key: encryption_key = get_config().RESOURCE_IDENTIFIER_ENCRYPTION_KEY return _aes_encrypt(encryption_key, "%s;%s" % (resource_type, data))
def get_semester_date(date: datetime.date) -> Tuple[str, int, int]: """获取日期对应的学期、所属周次及星期(0表示周日,1表示周一...) >>> get_semester_date(datetime.date(2020, 2, 22)) ('2019-2020-1', 26, 6) >>> get_semester_date(datetime.date(2020, 2, 23)) ('2019-2020-2', 1, 0) """ config = get_config() semesters = list(config.AVAILABLE_SEMESTERS.items()) semesters.sort(key=lambda x: x[0], reverse=True) for sem in semesters: sem_start_date = datetime.date(*sem[1]["start"]) if date >= sem_start_date: days_delta = (date - sem_start_date).days return "-".join([str(x) for x in sem[0] ]), days_delta // 7 + 1, days_delta % 7 raise ValueError("no applicable semester")
def decrypt(data: str, encryption_key: str = None, resource_type: str = None): """ 解密资源标识符 :param data: 加密后的字符串 :param encryption_key: 可选的 key :param resource_type: 验证资源类型(student、teacher、klass、room) :return: 资源类型和资源ID """ if not encryption_key: encryption_key = get_config().RESOURCE_IDENTIFIER_ENCRYPTION_KEY data = _aes_decrypt(encryption_key, data) group = re.match(r'^(student|teacher|klass|room);([\s\S]+)$', data) # 通过正则校验确定数据的正确性 if group is None: raise ValueError('Decrypted data is invalid: %s' % data) else: if resource_type and group.group(1) != resource_type: raise ValueError('Resource type not correspond') return group.group(1), group.group(2)
from everyclass.rpc import init as _init_rpc from everyclass.rpc.entity import Entity as _entity from everyclass.server.utils.config import get_config from everyclass.server.utils.encryption import encrypt _init_rpc(resource_id_encrypt_function=encrypt) # 为 everyclass.rpc 模块注入 encrypt 函数 _entity.set_base_url(get_config().ENTITY_BASE_URL) _entity.set_request_token(get_config().ENTITY_TOKEN)
def wrapped(*args, **kwargs): config = get_config() if config.MAINTENANCE: return render_template('maintenance.html') return func(*args, **kwargs)
import redis from everyclass.server.utils.config import get_config config = get_config() redis = redis.Redis(**config.REDIS) redis_prefix = "ec_sv"
import os import sys from logging.config import fileConfig from alembic import context from sqlalchemy import engine_from_config from sqlalchemy import pool sys.path.append( os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../'))) from everyclass.server.utils.db.postgres import Base, register_model_to_base from everyclass.server.utils.config import get_config _app_config = get_config() # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config # Interpret the config file for Python logging. # This line sets up loggers basically. fileConfig(config.config_file_name) # add your model's MetaData object here # for 'autogenerate' support # from myapp import mymodel # target_metadata = mymodel.Base.metadata register_model_to_base() target_metadata = Base.metadata
def get_connection() -> database.Database: """在连接池中获得连接""" if not has_app_context(): config = get_config() return MongoClient(**config.MONGODB).get_database(config.MONGODB_DB) return current_app.mongo.get_database(current_app.config['MONGODB_DB'])
def create_app() -> Flask: """创建 flask app""" from everyclass.server.utils.web_consts import MSG_INTERNAL_ERROR from everyclass.server import plugin_available from everyclass.server.utils import generate_error_response, api_helpers, base_exceptions from everyclass.common.env import is_production from everyclass.server.utils.web_consts import MSG_404 app = Flask(__name__, static_folder='../../frontend/dist', static_url_path='', template_folder="../../frontend/templates") # load app config from everyclass.server.utils.config import get_config _config = get_config() app.config.from_object(_config) # noqa: T484 """ 每课统一日志机制 规则如下: - DEBUG 模式下会输出 DEBUG 等级的日志,否则输出 INFO 及以上等级的日志 - 日志为 JSON 格式,会被节点上的 agent 采集并发送到 datadog,方便结合 metrics 和 APM 数据分析 - WARNING 以上级别的输出到 Sentry 做错误聚合 日志等级: critical – for errors that lead to termination error – for errors that occur, but are handled warning – for exceptional circumstances that might not be errors notice – for non-error messages you usually want to see info – for messages you usually don’t want to see debug – for debug messages Sentry: https://docs.sentry.io/clients/python/api/#raven.Client.captureMessage - stack 默认是 False """ if app.config['DEBUG']: logging.getLogger().setLevel(logging.DEBUG) else: logging.getLogger().setLevel(logging.INFO) # CDN CDN(app) # moment Moment(app) # encrypted session app.session_interface = EncryptedSessionInterface() # 导入并注册 blueprints from everyclass.server.calendar.views import calendar_bp from everyclass.server.calendar.views_api import calendar_api_bp from everyclass.server.entity.views import entity_bp from everyclass.server.entity.views_api import entity_api_bp from everyclass.server.user.views import user_bp from everyclass.server.user.views_api import user_api_bp from everyclass.server.course.views import course_bp from everyclass.server.course.views_api import course_api_bp from everyclass.server.views_main import main_blueprint app.register_blueprint(calendar_bp) app.register_blueprint(entity_bp) app.register_blueprint(user_bp, url_prefix='/user') app.register_blueprint(entity_api_bp, url_prefix='/mobile/entity') app.register_blueprint(user_api_bp, url_prefix='/mobile/user') app.register_blueprint(calendar_api_bp, url_prefix='/mobile/calendar') if 'course' in _config.FEATURE_GATING and _config.FEATURE_GATING['course']: app.register_blueprint(course_api_bp, url_prefix='/mobile/course') app.register_blueprint(course_bp, url_prefix='/course') app.register_blueprint(main_blueprint) # 初始化 RPC 模块 from everyclass.rpc.auth import Auth if 'AUTH_BASE_URL' in app.config: Auth.set_base_url(app.config['AUTH_BASE_URL']) @app.before_request def set_user_id(): """在请求之前设置 session uid,方便 APM 标识用户""" from everyclass.server.utils.web_consts import SESSION_CURRENT_USER, SESSION_USER_SEQ from everyclass.server.user import service as user_service if not session.get('user_id', None) and request.endpoint not in ( "main.health_check", "static"): logger.info( f"Give a new user ID for new user. endpoint: {request.endpoint}" ) session['user_id'] = user_service.get_user_id() if session.get('user_id', None): tracer.current_root_span().set_tag( "user_id", session[SESSION_USER_SEQ]) # 唯一用户 ID if session.get(SESSION_CURRENT_USER, None): tracer.current_root_span().set_tag( "username", session[SESSION_CURRENT_USER].identifier) # 学号或教工号 @app.before_request def log_request(): """日志中记录请求""" logger.info(f'Request received: {request.method} {request.path}') @app.after_request def response_minify(response): """用 htmlmin 压缩 HTML,减轻带宽压力""" if app.config[ 'HTML_MINIFY'] and response.content_type == u'text/html; charset=utf-8': response.set_data(minify(response.get_data(as_text=True))) return response from everyclass.server.utils.db.postgres import db_session @app.teardown_appcontext def shutdown_db_session(exception=None): db_session.remove() @app.template_filter('versioned') def version_filter(filename): """ 模板过滤器。如果 STATIC_VERSIONED,返回类似 'style-v1-c012dr.css' 的文件,而不是 'style-v1.css' :param filename: 文件名 :return: 新的文件名 """ if app.config['STATIC_VERSIONED']: if filename[:4] == 'css/': new_filename = app.config['STATIC_MANIFEST'][filename[4:]] return 'css/' + new_filename elif filename[:3] == 'js/': new_filename = app.config['STATIC_MANIFEST'][filename[3:]] return new_filename else: return app.config['STATIC_MANIFEST'][filename] return filename @app.context_processor def inject_consts(): """允许在模板中使用常量模块,以便使用session key等常量而不用在模板中硬编码""" return dict(consts=web_consts, api_base_url=app.config['MOBILE_API_BASE_URL']) @app.errorhandler(404) def page_not_found(error): if request.path.startswith('/mobile'): return generate_error_response( None, api_helpers.STATUS_CODE_INVALID_REQUEST, "no such API") return render_template('common/error.html', message=MSG_404) @app.errorhandler(base_exceptions.BizException) def handle_biz_exception(error: base_exceptions.BizException): if request.path.startswith("/mobile"): if isinstance(error, base_exceptions.InternalError): logger.error(repr(error)) # 业务错误的status_message可以对外展示 actual_error = {'status_message_overwrite': error.status_message} return generate_error_response(None, error.status_code, **actual_error) @app.errorhandler(500) def internal_server_error(error): if request.path.startswith("/mobile"): # 对于非业务错误,生产环境中不进行返回,其他环境中可返回 actual_error = { 'status_message_overwrite': f"server internal error: {repr(error.original_exception)}" } if not is_production() else {} return generate_error_response( None, api_helpers.STATUS_CODE_INTERNAL_ERROR, **actual_error) if plugin_available("sentry"): return render_template( 'common/error.html', message=MSG_INTERNAL_ERROR, event_id=g.sentry_event_id, public_dsn=sentry.client.get_public_dsn('https')) return f"<h4>500 Error: {repr(error.original_exception)}</h4><br>You are seeing this page because Sentry is not available." global __app __app = app return app
def test_import_config(self): from everyclass.server.utils.config import get_config config = get_config() self.assertTrue(config)