1016 lines
38 KiB
Python
1016 lines
38 KiB
Python
import os
|
||
import uuid
|
||
import json
|
||
import re
|
||
import importlib
|
||
import importlib.util
|
||
import traceback
|
||
import threading
|
||
from pathlib import Path
|
||
|
||
from django.http import HttpResponse, JsonResponse
|
||
from django.conf import settings
|
||
from django.urls import path
|
||
|
||
_request_local = threading.local()
|
||
|
||
|
||
def get_current_request():
|
||
return getattr(_request_local, 'request', None)
|
||
|
||
|
||
def set_current_request(request):
|
||
_request_local.request = request
|
||
|
||
|
||
class _CompatQueryDict:
|
||
def __init__(self, querydict):
|
||
self._qd = querydict
|
||
|
||
def get(self, key, default=None, type=None):
|
||
val = self._qd.get(key, default)
|
||
if val is None or val == default:
|
||
return val
|
||
if type is not None:
|
||
try:
|
||
return type(val)
|
||
except (ValueError, TypeError):
|
||
return default
|
||
return val
|
||
|
||
def __getitem__(self, key):
|
||
return self._qd[key]
|
||
|
||
def __contains__(self, key):
|
||
return key in self._qd
|
||
|
||
def __iter__(self):
|
||
return iter(self._qd)
|
||
|
||
def __bool__(self):
|
||
return bool(self._qd)
|
||
|
||
def keys(self):
|
||
return self._qd.keys()
|
||
|
||
def values(self):
|
||
return self._qd.values()
|
||
|
||
def items(self):
|
||
return self._qd.items()
|
||
|
||
def getlist(self, key):
|
||
return self._qd.getlist(key)
|
||
|
||
|
||
class _CompatHeaders:
|
||
def __init__(self, meta):
|
||
self._meta = meta
|
||
|
||
def get(self, key, default=None):
|
||
meta_key = 'HTTP_' + key.upper().replace('-', '_')
|
||
val = self._meta.get(meta_key)
|
||
if val is None:
|
||
meta_key = key.upper().replace('-', '_')
|
||
val = self._meta.get(meta_key)
|
||
return val if val is not None else default
|
||
|
||
def __getitem__(self, key):
|
||
val = self.get(key)
|
||
if val is None:
|
||
raise KeyError(key)
|
||
return val
|
||
|
||
def __contains__(self, key):
|
||
return self.get(key) is not None
|
||
|
||
|
||
class _CompatRequest:
|
||
def __init__(self, django_request):
|
||
self._req = django_request
|
||
self.method = django_request.method
|
||
try:
|
||
self._body = django_request.body
|
||
except Exception:
|
||
self._body = b''
|
||
if self.method in ("PATCH", "PUT") and django_request.content_type == "application/x-www-form-urlencoded":
|
||
from django.http import QueryDict as _QD
|
||
self.POST = _CompatQueryDict(_QD(self._body, encoding="utf-8"))
|
||
else:
|
||
self.POST = _CompatQueryDict(django_request.POST)
|
||
self.GET = _CompatQueryDict(django_request.GET)
|
||
self.COOKIES = django_request.COOKIES
|
||
self.META = django_request.META
|
||
self.FILES = django_request.FILES
|
||
self.path = django_request.path
|
||
|
||
@property
|
||
def body(self):
|
||
return self._body
|
||
|
||
def build_absolute_uri(self):
|
||
return self._req.build_absolute_uri()
|
||
|
||
def get_host(self):
|
||
return self._req.get_host()
|
||
|
||
def get_json(self, silent=False, force=False):
|
||
try:
|
||
if self.body:
|
||
return json.loads(self.body.decode('utf-8'))
|
||
if silent:
|
||
return None
|
||
raise ValueError("Empty request body")
|
||
except Exception:
|
||
if not silent:
|
||
raise
|
||
return None
|
||
|
||
@property
|
||
def form(self):
|
||
return self.POST
|
||
|
||
@property
|
||
def args(self):
|
||
return self.GET
|
||
|
||
@property
|
||
def json(self):
|
||
result = self.get_json(silent=True)
|
||
return result if result is not None else {}
|
||
|
||
@property
|
||
def cookies(self):
|
||
return self.COOKIES
|
||
|
||
@property
|
||
def headers(self):
|
||
return _CompatHeaders(self.META)
|
||
|
||
@property
|
||
def files(self):
|
||
return self.FILES
|
||
|
||
def get_data(self, as_text=False):
|
||
if as_text:
|
||
return self.body.decode('utf-8')
|
||
return self.body
|
||
|
||
@property
|
||
def url(self):
|
||
return self.build_absolute_uri()
|
||
|
||
@property
|
||
def referrer(self):
|
||
return self.META.get('HTTP_REFERER', '')
|
||
|
||
@property
|
||
def host(self):
|
||
return self.get_host()
|
||
|
||
|
||
def _inject_templateapi_imports(source, filepath):
|
||
rel = os.path.relpath(filepath, settings.BASE_DIR)
|
||
parts = Path(rel).parts
|
||
|
||
has_formdata = ('import TempLateAPI.GERP.API.FormAPI.FormData' in source
|
||
or 'from TempLateAPI.GERP.API.FormAPI' in source)
|
||
|
||
if has_formdata or 'FormAPI' in parts:
|
||
source = source.replace(
|
||
'import TempLateAPI.GERP.API.FormAPI.FormData as FormData',
|
||
'# import FormData - will be loaded dynamically'
|
||
)
|
||
source = source.replace(
|
||
'from TempLateAPI.GERP.API.FormAPI import FormData',
|
||
'# import FormData - will be loaded dynamically'
|
||
)
|
||
source = source.replace(
|
||
'import TempLateAPI.GERP.API.FormAPI.FormData',
|
||
'# import FormData - will be loaded dynamically'
|
||
)
|
||
lines = source.split('\n')
|
||
insert_pos = 0
|
||
for i, line in enumerate(lines):
|
||
stripped = line.strip()
|
||
if stripped.startswith('# import FormData') or stripped.startswith('from PDAPI'):
|
||
insert_pos = i + 1
|
||
if insert_pos > 0:
|
||
compat_code = (
|
||
'\ntry:\n'
|
||
' from gvsds.fsr_compat import FormDataCompat\n'
|
||
' FormData = FormDataCompat()\n'
|
||
'except Exception as _fd_err:\n'
|
||
' import types\n'
|
||
' FormData = types.SimpleNamespace()\n'
|
||
' FormData.GetTable = lambda *a, **k: types.SimpleNamespace(\n'
|
||
' KeyToUUIDConversion=lambda *a2, **k2: "",\n'
|
||
' GetItem=lambda *a3, **k3: {},\n'
|
||
' )\n'
|
||
' FormData.TABLE_CODE_DISPLAY = {}\n'
|
||
' FormData.TABLE_ENUM_LABELS = {}\n'
|
||
' FormData.TABLE_FK_PICKER = {}\n'
|
||
' FormData.get_user_timezone = lambda: "UTC"\n'
|
||
' FormData.utc_to_user_tz = lambda dt, tz=None: dt\n'
|
||
' FormData.RegisterFunctionExtension = lambda *a, **k: None\n'
|
||
)
|
||
lines.insert(insert_pos, compat_code)
|
||
source = '\n'.join(lines)
|
||
|
||
source = source.replace('import TempLateAPI.', '# import TempLateAPI.')
|
||
source = source.replace('from TempLateAPI.', '# from TempLateAPI.')
|
||
|
||
return source
|
||
|
||
|
||
class _AbortException(Exception):
|
||
def __init__(self, response):
|
||
self.response = response
|
||
|
||
|
||
class _ExtractExpr:
|
||
def __init__(self, field_name, lookup):
|
||
self.field_name = field_name
|
||
self.lookup = lookup
|
||
|
||
def __eq__(self, other):
|
||
from django.db.models import Q
|
||
return Q(**{f'{self.field_name}__{self.lookup}': other})
|
||
|
||
def __ne__(self, other):
|
||
from django.db.models import Q
|
||
return ~Q(**{f'{self.field_name}__{self.lookup}': other})
|
||
|
||
def __gt__(self, other):
|
||
from django.db.models import Q
|
||
return Q(**{f'{self.field_name}__{self.lookup}__gt': other})
|
||
|
||
def __lt__(self, other):
|
||
from django.db.models import Q
|
||
return Q(**{f'{self.field_name}__{self.lookup}__lt': other})
|
||
|
||
def __ge__(self, other):
|
||
from django.db.models import Q
|
||
return Q(**{f'{self.field_name}__{self.lookup}__gte': other})
|
||
|
||
def __le__(self, other):
|
||
from django.db.models import Q
|
||
return Q(**{f'{self.field_name}__{self.lookup}__lte': other})
|
||
|
||
|
||
class _AggregateExpr:
|
||
def __init__(self, model, aggregate):
|
||
self.model = model
|
||
self.aggregate = aggregate
|
||
|
||
|
||
class _CompatFunc:
|
||
def sum(self, field_expr):
|
||
from django.db.models import Sum
|
||
if hasattr(field_expr, '_django_name'):
|
||
agg = Sum(field_expr._django_name)
|
||
elif hasattr(field_expr, 'field_name'):
|
||
agg = Sum(field_expr.field_name)
|
||
else:
|
||
agg = Sum(str(field_expr))
|
||
model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None
|
||
return _AggregateExpr(model, agg) if model else agg
|
||
|
||
def count(self, field_expr=None):
|
||
from django.db.models import Count
|
||
if field_expr is None:
|
||
return Count('*')
|
||
if hasattr(field_expr, '_django_name'):
|
||
agg = Count(field_expr._django_name)
|
||
else:
|
||
agg = Count(str(field_expr))
|
||
model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None
|
||
return _AggregateExpr(model, agg) if model else agg
|
||
|
||
def max(self, field_expr):
|
||
from django.db.models import Max
|
||
if hasattr(field_expr, '_django_name'):
|
||
agg = Max(field_expr._django_name)
|
||
else:
|
||
agg = Max(str(field_expr))
|
||
model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None
|
||
return _AggregateExpr(model, agg) if model else agg
|
||
|
||
def min(self, field_expr):
|
||
from django.db.models import Min
|
||
if hasattr(field_expr, '_django_name'):
|
||
agg = Min(field_expr._django_name)
|
||
else:
|
||
agg = Min(str(field_expr))
|
||
model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None
|
||
return _AggregateExpr(model, agg) if model else agg
|
||
|
||
def avg(self, field_expr):
|
||
from django.db.models import Avg
|
||
if hasattr(field_expr, '_django_name'):
|
||
agg = Avg(field_expr._django_name)
|
||
else:
|
||
agg = Avg(str(field_expr))
|
||
model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None
|
||
return _AggregateExpr(model, agg) if model else agg
|
||
|
||
def year(self, field_expr):
|
||
if hasattr(field_expr, '_django_name'):
|
||
return _ExtractExpr(field_expr._django_name, 'year')
|
||
return _ExtractExpr(str(field_expr), 'year')
|
||
|
||
def month(self, field_expr):
|
||
if hasattr(field_expr, '_django_name'):
|
||
return _ExtractExpr(field_expr._django_name, 'month')
|
||
return _ExtractExpr(str(field_expr), 'month')
|
||
|
||
def day(self, field_expr):
|
||
if hasattr(field_expr, '_django_name'):
|
||
return _ExtractExpr(field_expr._django_name, 'day')
|
||
return _ExtractExpr(str(field_expr), 'day')
|
||
|
||
|
||
FLASK_COMPAT_HEADER = """
|
||
from django.http import HttpResponse, JsonResponse as _DJJsonResponse, FileResponse as _FileResponse, StreamingHttpResponse as _StreamingHttpResponse
|
||
from django.db.models import Q as _QDj
|
||
import json as _json
|
||
import re as _re
|
||
import threading as _threading
|
||
import os as _os
|
||
from gvsds.fsr import _CompatRequest as _CompatRequest, _CompatQueryDict as _CompatQueryDict
|
||
from gvsds.fsr import _ExtractExpr as _ExtractExpr, _CompatFunc as _CompatFunc, _AggregateExpr as _AggregateExpr
|
||
from gvsds.fsr import _AbortException as _AbortException
|
||
from PDAPI_django import _DeleteStmt as _DeleteStmt
|
||
|
||
_request_local = _threading.local()
|
||
_api_local = _threading.local()
|
||
|
||
def _Q(model):
|
||
from gvsds.fluent import FluentQuery
|
||
return FluentQuery(model)
|
||
|
||
def _jsonify(*args, **kwargs):
|
||
if args and kwargs:
|
||
raise TypeError('jsonify() behavior undefined when passed both args and kwargs')
|
||
elif len(args) == 1:
|
||
data = args[0]
|
||
else:
|
||
data = args or kwargs
|
||
return _DJJsonResponse(data, safe=False)
|
||
|
||
def _redirect(location, code=302):
|
||
from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
|
||
if code == 301:
|
||
return HttpResponsePermanentRedirect(location)
|
||
return HttpResponseRedirect(location)
|
||
|
||
def _url_for(endpoint, **kwargs):
|
||
endpoint_map = {
|
||
'MainLoginHTML': 'https://ugm.gvsds.com/login.html',
|
||
'GERP.GERPIndex': 'https://gerp.gvsds.com/',
|
||
'UGM.Index': 'https://ugm.gvsds.com/',
|
||
'main': 'https://www.gvsds.com/',
|
||
}
|
||
if endpoint in endpoint_map:
|
||
return endpoint_map[endpoint]
|
||
parts = endpoint.split('.')
|
||
if len(parts) >= 2:
|
||
subdomain = parts[0].lower()
|
||
path = '/'.join(parts[1:]).lower()
|
||
return f'https://{subdomain}.gvsds.com/{path}'
|
||
return f'/'
|
||
|
||
def _make_response(rv, status=None, headers=None):
|
||
if isinstance(rv, dict):
|
||
resp = _DJJsonResponse(rv, safe=False)
|
||
elif isinstance(rv, str):
|
||
resp = HttpResponse(rv)
|
||
elif isinstance(rv, bytes):
|
||
resp = HttpResponse(rv)
|
||
else:
|
||
resp = HttpResponse(str(rv))
|
||
if status:
|
||
resp.status_code = status
|
||
if headers:
|
||
for k, v in headers.items():
|
||
resp[k] = v
|
||
return resp
|
||
|
||
def _send_file(filepath_or_obj, mimetype=None, as_attachment=False, download_name=None, conditional=True):
|
||
if isinstance(filepath_or_obj, str):
|
||
from django.core.files import File as _DjFile
|
||
f = open(filepath_or_obj, 'rb')
|
||
resp = _FileResponse(f, content_type=mimetype or 'application/octet-stream')
|
||
elif hasattr(filepath_or_obj, 'read'):
|
||
resp = _FileResponse(filepath_or_obj, content_type=mimetype or 'application/octet-stream')
|
||
else:
|
||
resp = HttpResponse(filepath_or_obj, content_type=mimetype or 'application/octet-stream')
|
||
if as_attachment and download_name:
|
||
resp['Content-Disposition'] = f'attachment; filename="{download_name}"'
|
||
return resp
|
||
|
||
def _FlaskResponse(content=None, status=None, mimetype=None, headers=None):
|
||
content_type = mimetype or 'text/html'
|
||
if isinstance(content, bytes):
|
||
resp = HttpResponse(content, content_type=content_type, status=status or 200)
|
||
elif isinstance(content, str):
|
||
resp = HttpResponse(content, content_type=content_type, status=status or 200)
|
||
else:
|
||
resp = HttpResponse(content, content_type=content_type, status=status or 200)
|
||
if headers:
|
||
for k, v in headers.items():
|
||
resp[k] = v
|
||
return resp
|
||
|
||
class _g:
|
||
@property
|
||
def ELTUser(self):
|
||
elt_user = getattr(_api_local, 'elt_user', None)
|
||
if elt_user is not None:
|
||
return elt_user
|
||
from PDAPI_django import UGM
|
||
return UGM.GetELTUser()
|
||
|
||
class _session(dict):
|
||
pass
|
||
|
||
def _abort(code, description=None):
|
||
raise _AbortException(HttpResponse(description or '', status=code))
|
||
|
||
class _current_app:
|
||
config = {}
|
||
|
||
class _FlaskCompatObj:
|
||
@property
|
||
def request(self):
|
||
return getattr(_api_local, 'request', None)
|
||
|
||
Response = staticmethod(_FlaskResponse)
|
||
send_file = staticmethod(_send_file)
|
||
jsonify = staticmethod(_jsonify)
|
||
redirect = staticmethod(_redirect)
|
||
url_for = staticmethod(_url_for)
|
||
make_response = staticmethod(_make_response)
|
||
abort = staticmethod(_abort)
|
||
g = _g()
|
||
session = _session()
|
||
current_app = _current_app()
|
||
flash = lambda *a, **k: None
|
||
Blueprint = type('Blueprint', (), {})
|
||
Flask = type('Flask', (), {})
|
||
|
||
flask = _FlaskCompatObj()
|
||
|
||
func = _CompatFunc()
|
||
|
||
def _regex_filter(field_name, value):
|
||
from PDAPI_django import _pascal_to_snake
|
||
snake = _pascal_to_snake(field_name)
|
||
return _QDj(**{snake + '__regex': value})
|
||
|
||
class _ParameterValidator:
|
||
DEFAULT_TIMESTAMP_STR_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||
|
||
class _FSRCompat:
|
||
ParameterValidator = _ParameterValidator()
|
||
|
||
FSR = _FSRCompat()
|
||
"""
|
||
|
||
|
||
class StringLoader(importlib.util.Loader):
|
||
def __init__(self, source, fullname):
|
||
self.source = source
|
||
self.fullname = fullname
|
||
|
||
def create_module(self, spec):
|
||
return None
|
||
|
||
def exec_module(self, module):
|
||
code = compile(self.source, f"<api:{self.fullname}>", "exec")
|
||
exec(code, module.__dict__)
|
||
|
||
|
||
def _load_api_module(filepath, force_reload=False):
|
||
with open(filepath, 'r', encoding='utf-8') as f:
|
||
source = f.read()
|
||
|
||
source = source.replace('from PDAPI import *', 'from PDAPI_django import *')
|
||
source = source.replace('import PDAPI', 'import PDAPI_django as PDAPI')
|
||
source = source.replace('from AppBridge import *', '')
|
||
source = source.replace('import AppBridge', '')
|
||
|
||
source = source.replace('flask.request.method', 'request.method')
|
||
source = source.replace('flask.request.form', 'request.POST')
|
||
source = source.replace('flask.request.args', 'request.GET')
|
||
source = source.replace('flask.request.json', 'request.json')
|
||
source = source.replace('flask.request.cookies', 'request.COOKIES')
|
||
source = source.replace('flask.request.headers', 'request.headers')
|
||
source = source.replace('flask.request.files', 'request.FILES')
|
||
source = source.replace('flask.request.url', 'request.build_absolute_uri()')
|
||
source = source.replace('flask.request.path', 'request.path')
|
||
source = source.replace('flask.request.referrer', 'request.META.get("HTTP_REFERER", "")')
|
||
source = source.replace('flask.request.host', 'request.get_host()')
|
||
source = source.replace('flask.request.get_json(silent=True)', 'request.get_json(silent=True)')
|
||
source = source.replace('flask.request.get_json()', 'request.get_json()')
|
||
source = source.replace('flask.request', 'request')
|
||
|
||
source = source.replace('flask.jsonify', '_jsonify')
|
||
source = source.replace('flask.redirect', '_redirect')
|
||
source = source.replace('flask.url_for', '_url_for')
|
||
source = source.replace('flask.make_response', '_make_response')
|
||
source = source.replace('flask.g', '_g')
|
||
source = source.replace('flask.session', '_session')
|
||
source = source.replace('flask.abort', '_abort')
|
||
source = source.replace('flask.current_app', '_current_app')
|
||
source = source.replace('flask.Blueprint', 'type("Blueprint", (), {})')
|
||
source = source.replace('flask.Flask', 'type("Flask", (), {})')
|
||
source = source.replace('flask.flash', 'lambda *a, **k: None')
|
||
|
||
source = source.replace('from flask import', '# from flask import')
|
||
source = source.replace('import flask', '# import flask')
|
||
source = source.replace('flask_wtf', 'type("flask_wtf", (), {})')
|
||
source = source.replace('flask_socketio', 'type("flask_socketio", (), {})')
|
||
source = source.replace('flask_cors', 'type("flask_cors", (), {})')
|
||
source = source.replace('flask_sqlalchemy', 'type("flask_sqlalchemy", (), {})')
|
||
source = source.replace('flask_bcrypt', 'type("flask_bcrypt", (), {})')
|
||
source = source.replace('flask_limiter', 'type("flask_limiter", (), {})')
|
||
|
||
source = source.replace('from Utils.Models import', 'from gvsds.apps.gerp.models import')
|
||
source = source.replace('from Utils.Object import UUIDObj, NewBUUID', 'from PDAPI_django import UUIDObj, NewBUUID')
|
||
source = source.replace('from Utils.Object import UUIDObj', 'from PDAPI_django import UUIDObj')
|
||
source = source.replace('from Utils.Object import NewBUUID', 'from PDAPI_django import NewBUUID')
|
||
source = source.replace('from Utils.Object import Get16toBytes, GetBytesTo16', 'from PDAPI_django import Get16toBytes, GetBytesTo16')
|
||
source = source.replace('from Utils.Object import Get16toBytes', 'from PDAPI_django import Get16toBytes')
|
||
source = source.replace('from Utils.Object import GetBytesTo16', 'from PDAPI_django import GetBytesTo16')
|
||
source = source.replace('from Utils.Object import GenerateUUID', 'from PDAPI_django import GenerateUUID')
|
||
source = re.sub(r'from Utils\.Object import (.+)', r'from PDAPI_django import \1', source)
|
||
source = source.replace('from Utils.Lib import', 'from PDAPI_django import')
|
||
source = source.replace('from Utils.UGMLib import', 'from PDAPI_django import')
|
||
source = source.replace('from Utils.Browser import', 'from PDAPI_django import')
|
||
source = source.replace('from Utils.Config import', 'from PDAPI_django import')
|
||
source = source.replace('from Utils.Form import', 'from PDAPI_django import')
|
||
source = source.replace('from Utils.Safe import', 'from PDAPI_django import')
|
||
source = source.replace('from Utils.Validation import', 'from PDAPI_django import')
|
||
source = source.replace('from Utils.Email import', 'from PDAPI_django import')
|
||
source = source.replace('from Utils.Template import', 'from PDAPI_django import')
|
||
source = source.replace('from Utils.Permission import', 'from PDAPI_django import')
|
||
source = source.replace('from Utils.Constants import', 'from PDAPI_django import')
|
||
|
||
source = re.sub(r'([\w.]+)\.query\.filter\(', r'_Q(\1).filter(', source)
|
||
source = re.sub(r'([\w.]+)\.query\.order_by\(', r'_Q(\1).order_by(', source)
|
||
source = re.sub(r'([\w.]+)\.query\.all\(\)', r'_Q(\1).all()', source)
|
||
source = re.sub(r'([\w.]+)\.query\.first\(\)', r'_Q(\1).first()', source)
|
||
source = re.sub(r'([\w.]+)\.query\.count\(\)', r'_Q(\1).count()', source)
|
||
source = re.sub(r'([\w.]+)\.query\.get\(', r'_Q(\1).get(', source)
|
||
source = re.sub(r'([\w.]+)\.query\.limit\(', r'_Q(\1).limit(', source)
|
||
source = re.sub(r'([\w.]+)\.query\.join\(', r'_Q(\1).join(', source)
|
||
source = re.sub(r'([\w.]+)\.query\.paginate\(', r'_Q(\1).paginate(', source)
|
||
|
||
source = source.replace('from sqlalchemy import func', 'func = func')
|
||
source = source.replace('from sqlalchemy import', '# from sqlalchemy import')
|
||
source = source.replace('import sqlalchemy', '# import sqlalchemy')
|
||
|
||
source = source.replace('sqlalchemy.and_', 'db.and_')
|
||
source = source.replace('sqlalchemy.or_', 'db.or_')
|
||
source = source.replace('sqlalchemy.exc.IntegrityError', 'django.db.IntegrityError')
|
||
source = re.sub(r'sqlalchemy\.delete\((\w+)\)', r'_DeleteStmt(\1)', source)
|
||
source = source.replace('sqlalchemy.orm.joinedload', '# joinedload')
|
||
source = re.sub(
|
||
r'\.filter\(sqlalchemy\.text\(f"\{(\w+)\}\s+REGEXP\s+:(\w+)"\)\)\.params\(\2=(\w+)\)',
|
||
r'.filter(_regex_filter(\1, \3))',
|
||
source
|
||
)
|
||
source = re.sub(r'sqlalchemy\.text\(.+?\)(?:\.params\(.+?\))?', 'Q()', source)
|
||
source = source.replace('.filter_by(', '.filter(')
|
||
source = source.replace('.with_entities(', '.values(')
|
||
source = source.replace('.ilike(', '.like(')
|
||
|
||
source = _inject_templateapi_imports(source, filepath)
|
||
|
||
source = source.replace('from TempLateModule.', '# from TempLateModule.')
|
||
|
||
source = FLASK_COMPAT_HEADER + "\n" + source
|
||
|
||
_load_api_module._last_source = source
|
||
|
||
rel_path = os.path.relpath(filepath, settings.BASE_DIR)
|
||
module_name = f"api_dynamic.{rel_path.replace(os.sep, '.').replace('/', '.').replace('.api', '')}"
|
||
|
||
spec = importlib.util.spec_from_loader(module_name, StringLoader(source, module_name))
|
||
module = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(module)
|
||
|
||
return module
|
||
|
||
|
||
def _generate_route(filepath, api_dir, allow_ignore=None):
|
||
rel = os.path.relpath(filepath, api_dir)
|
||
rel = rel.replace(os.sep, '/')
|
||
parts = rel.replace('.api', '').split('/')
|
||
|
||
if allow_ignore and parts and parts[0] == allow_ignore:
|
||
parts = parts[1:]
|
||
|
||
route = '/' + '/'.join(parts)
|
||
return route
|
||
|
||
|
||
def _uuid_to_str(val):
|
||
if val is None:
|
||
return None
|
||
if isinstance(val, str):
|
||
return val
|
||
if isinstance(val, bytes) and len(val) == 16:
|
||
return str(uuid.UUID(bytes=val))
|
||
if isinstance(val, uuid.UUID):
|
||
return str(val)
|
||
return str(val)
|
||
|
||
|
||
def _uuid_to_bytes(val):
|
||
if val is None:
|
||
return None
|
||
if isinstance(val, bytes):
|
||
return val
|
||
if isinstance(val, str):
|
||
try:
|
||
return uuid.UUID(val).bytes
|
||
except ValueError:
|
||
return None
|
||
if isinstance(val, uuid.UUID):
|
||
return val.bytes
|
||
return None
|
||
|
||
|
||
class DjangoFSR:
|
||
_instance = None
|
||
|
||
def __new__(cls):
|
||
if cls._instance is None:
|
||
cls._instance = super().__new__(cls)
|
||
cls._instance._initialized = False
|
||
return cls._instance
|
||
|
||
def __init__(self):
|
||
if self._initialized:
|
||
return
|
||
self._initialized = True
|
||
self.api_dir = str(Path(settings.BASE_DIR) / "TempLateAPI")
|
||
self.blueprint_map = {
|
||
"GERP": {"allow_ignore": "GERP"},
|
||
"UGM": {"allow_ignore": "UGM"},
|
||
"3CRB": {"allow_ignore": "3CRB"},
|
||
"Invite": {"allow_ignore": "Invite"},
|
||
"CCGO": {"allow_ignore": "CCGO"},
|
||
"SVE": {"allow_ignore": "SVE"},
|
||
"Verifox": {"allow_ignore": "Verifox"},
|
||
"Main": {"allow_ignore": None},
|
||
}
|
||
self._modules_cache = {}
|
||
self._routes = []
|
||
|
||
def _get_blueprint_config(self, filepath):
|
||
rel = os.path.relpath(filepath, self.api_dir)
|
||
parts = rel.replace(os.sep, '/').split('/')
|
||
if parts:
|
||
top_dir = parts[0]
|
||
for bp_name, config in self.blueprint_map.items():
|
||
if top_dir == config.get("allow_ignore") or top_dir == bp_name:
|
||
return bp_name, config
|
||
return "Main", {"allow_ignore": None}
|
||
|
||
def _validate_params(self, request, params_config, source, path_kwargs=None):
|
||
cleaned = {}
|
||
if not params_config:
|
||
return cleaned, None
|
||
|
||
if callable(params_config):
|
||
try:
|
||
params_config = params_config(**(path_kwargs or {}))
|
||
except Exception:
|
||
params_config = []
|
||
|
||
if not isinstance(params_config, list):
|
||
if isinstance(params_config, tuple) and len(params_config) == 2:
|
||
resp_body, status_code = params_config
|
||
return cleaned, (resp_body, status_code)
|
||
if isinstance(params_config, dict):
|
||
return cleaned, (params_config, 400)
|
||
params_config = []
|
||
|
||
for param in params_config:
|
||
name = param.get("Name", "")
|
||
ptype = param.get("Type", "str")
|
||
required = param.get("Required", False)
|
||
max_len = param.get("MaxLen")
|
||
allowed_empty = param.get("AllowedEmpty", False)
|
||
pass_empty = param.get("PassEmpty", False)
|
||
|
||
if source == "form":
|
||
raw = request.POST.get(name)
|
||
elif source == "args":
|
||
raw = request.GET.get(name)
|
||
elif source == "json":
|
||
try:
|
||
body = json.loads(request.body.decode('utf-8')) if request.body else {}
|
||
raw = body.get(name)
|
||
except Exception:
|
||
raw = None
|
||
elif source == "headers":
|
||
raw = request.META.get(f'HTTP_{name.upper().replace("-", "_")}')
|
||
elif source == "cookies":
|
||
raw = request.COOKIES.get(name)
|
||
else:
|
||
raw = None
|
||
|
||
if raw is None or (isinstance(raw, str) and not raw.strip()):
|
||
if pass_empty:
|
||
continue
|
||
if not required:
|
||
continue
|
||
if allowed_empty:
|
||
cleaned[name] = ""
|
||
continue
|
||
return cleaned, ({"code": 400, "msg": f"参数 {name} 缺失"}, 400)
|
||
|
||
if isinstance(raw, str):
|
||
raw = raw.strip()
|
||
|
||
if max_len and isinstance(raw, str) and len(raw) > max_len:
|
||
return cleaned, ({"code": 400, "msg": f"参数 {name} 超过最大长度 {max_len}"}, 400)
|
||
|
||
if ptype == "int":
|
||
try:
|
||
raw = int(raw)
|
||
except (ValueError, TypeError):
|
||
return cleaned, ({"code": 400, "msg": f"参数 {name} 类型错误(预期int)"}, 400)
|
||
elif ptype == "float":
|
||
try:
|
||
raw = float(raw)
|
||
except (ValueError, TypeError):
|
||
return cleaned, ({"code": 400, "msg": f"参数 {name} 类型错误(预期float)"}, 400)
|
||
elif ptype == "timestampstr":
|
||
import datetime as _dt
|
||
try:
|
||
if isinstance(raw, str):
|
||
for fmt in ["%Y-%m-%d %H:%M:%S", "%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S.%f"]:
|
||
try:
|
||
raw = _dt.datetime.strptime(raw, fmt).strftime("%Y-%m-%d %H:%M:%S")
|
||
break
|
||
except ValueError:
|
||
continue
|
||
else:
|
||
return cleaned, ({"code": 422, "msg": f"参数 {name} 类型错误(预期timestampstr): 时间字符串格式错误,预期格式: %Y-%m-%d %H:%M:%S,实际值: {raw}"}, 422)
|
||
except Exception as e:
|
||
return cleaned, ({"code": 422, "msg": f"参数 {name} 类型错误(预期timestampstr): {str(e)}"}, 422)
|
||
|
||
cleaned[name] = raw
|
||
|
||
return cleaned, None
|
||
|
||
def _check_permission(self, request, permission, attributes):
|
||
if not permission:
|
||
return True
|
||
|
||
elt_token = request.COOKIES.get('ELToken')
|
||
if not elt_token:
|
||
return False
|
||
|
||
try:
|
||
from gvsds.apps.core.models import User, EphemeralToken
|
||
elt_uuid = _uuid_to_bytes(elt_token)
|
||
elt = EphemeralToken.objects.filter(elt_uuid=elt_uuid, elt_status=1).first()
|
||
if not elt or elt.is_expired():
|
||
return False
|
||
|
||
user = User.objects.filter(user_uuid=elt.elt_user_uuid).first()
|
||
if not user:
|
||
return False
|
||
|
||
request.elt_user = user
|
||
request.elt = elt
|
||
|
||
if permission >= 1 and user.user_account_license != 1:
|
||
return False
|
||
|
||
if attributes and isinstance(attributes, list):
|
||
for attr in attributes:
|
||
if isinstance(attr, str):
|
||
if not user.has_permission(attr):
|
||
return False
|
||
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
def _create_django_view(self, filepath, module):
|
||
metadata = getattr(module, '__metadata__', {})
|
||
route_template = metadata.get("route", "{DEFAULT_ROUTE}")
|
||
methods = metadata.get("methods", ["GET"])
|
||
blueprint_name = metadata.get("blueprint", "Main")
|
||
permission = metadata.get("permission", 0)
|
||
attributes = metadata.get("attributes", [])
|
||
limit = metadata.get("limit", None)
|
||
|
||
bp_name, bp_config = self._get_blueprint_config(filepath)
|
||
default_route = _generate_route(filepath, self.api_dir, bp_config.get("allow_ignore"))
|
||
|
||
if "{DEFAULT_ROUTE}" in route_template:
|
||
route = route_template.replace("{DEFAULT_ROUTE}", default_route)
|
||
else:
|
||
route = route_template
|
||
|
||
fsr = self
|
||
|
||
def api_view(request, **kwargs):
|
||
if request.method == 'OPTIONS':
|
||
resp = HttpResponse(status=204)
|
||
origin = request.META.get('HTTP_ORIGIN', '')
|
||
if origin and '.gvsds.com' in origin:
|
||
resp['Access-Control-Allow-Origin'] = origin
|
||
resp['Access-Control-Allow-Credentials'] = 'true'
|
||
resp['Access-Control-Allow-Methods'] = 'GET, POST, PUT, PATCH, DELETE, OPTIONS'
|
||
resp['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, X-Requested-With'
|
||
resp['Access-Control-Max-Age'] = '86400'
|
||
return resp
|
||
|
||
from PDAPI_django import set_current_request as _set_req
|
||
compat_request = _CompatRequest(request)
|
||
_set_req(compat_request)
|
||
|
||
try:
|
||
current_mtime = os.path.getmtime(filepath)
|
||
cache_key = filepath
|
||
cached = fsr._modules_cache.get(cache_key)
|
||
if cached and cached[1] >= current_mtime:
|
||
mod = cached[0]
|
||
else:
|
||
mod = _load_api_module(filepath, force_reload=True)
|
||
fsr._modules_cache[cache_key] = (mod, current_mtime)
|
||
|
||
meta = getattr(mod, '__metadata__', {})
|
||
perm = meta.get("permission", 0)
|
||
attrs = meta.get("attributes", [])
|
||
|
||
if not fsr._check_permission(compat_request, perm, attrs):
|
||
return JsonResponse({"code": 403, "msg": "权限不足"}, status=403)
|
||
|
||
form_params = meta.get("form", [])
|
||
args_params = meta.get("args", [])
|
||
json_params = meta.get("json", [])
|
||
header_params = meta.get("headers", [])
|
||
cookie_params = meta.get("cookies", [])
|
||
|
||
form_data, form_err = fsr._validate_params(compat_request, form_params, "form", kwargs)
|
||
if form_err:
|
||
return JsonResponse(form_err[0], status=form_err[1])
|
||
|
||
args_data, args_err = fsr._validate_params(compat_request, args_params, "args", kwargs)
|
||
if args_err:
|
||
return JsonResponse(args_err[0], status=args_err[1])
|
||
|
||
json_data, json_err = fsr._validate_params(compat_request, json_params, "json", kwargs)
|
||
if json_err:
|
||
return JsonResponse(json_err[0], status=json_err[1])
|
||
|
||
header_data, header_err = fsr._validate_params(compat_request, header_params, "headers", kwargs)
|
||
if header_err:
|
||
return JsonResponse(header_err[0], status=header_err[1])
|
||
|
||
cookie_data, cookie_err = fsr._validate_params(compat_request, cookie_params, "cookies", kwargs)
|
||
if cookie_err:
|
||
return JsonResponse(cookie_err[0], status=cookie_err[1])
|
||
|
||
cleaned = {
|
||
"form": form_data,
|
||
"args": args_data,
|
||
"json": json_data,
|
||
"headers": header_data,
|
||
"cookies": cookie_data,
|
||
"params": kwargs,
|
||
}
|
||
|
||
handle = getattr(mod, 'HandleRequest', None)
|
||
if not handle or not callable(handle):
|
||
return JsonResponse({"code": 0, "msg": "ok"})
|
||
|
||
mod_api_local = getattr(mod, '_api_local', None)
|
||
if mod_api_local is not None:
|
||
mod_api_local.request = compat_request
|
||
mod_api_local.elt_user = getattr(compat_request, 'elt_user', None)
|
||
_api_local.request = compat_request
|
||
mod.request = compat_request
|
||
|
||
try:
|
||
import inspect
|
||
sig = inspect.signature(handle)
|
||
if len(sig.parameters) > 0:
|
||
result = handle(cleaned)
|
||
else:
|
||
result = handle()
|
||
except _AbortException as abort_exc:
|
||
result = abort_exc.response
|
||
except Exception as e:
|
||
tb = traceback.format_exc()
|
||
api_src = getattr(_load_api_module, '_last_source', '')
|
||
src_lines = api_src.split('\n') if api_src else []
|
||
tb_lines = tb.split('\n')
|
||
for line in tb_lines:
|
||
if line.strip().startswith('File "<api:'):
|
||
import re as _re
|
||
m = _re.search(r'line (\d+)', line)
|
||
if m:
|
||
err_lineno = int(m.group(1))
|
||
start = max(0, err_lineno - 3)
|
||
end = min(len(src_lines), err_lineno + 3)
|
||
tb += f"\n Source context:\n"
|
||
for i in range(start, end):
|
||
marker = ">>>" if i == err_lineno - 1 else " "
|
||
tb += f" {marker} {i+1}: {src_lines[i]}\n"
|
||
print(tb)
|
||
return JsonResponse({"code": 500, "msg": str(e)}, status=500)
|
||
|
||
if isinstance(result, tuple):
|
||
data, status_code = result[0], result[1] if len(result) > 1 else 200
|
||
if isinstance(data, dict):
|
||
resp = JsonResponse(data, status=status_code, safe=False)
|
||
elif isinstance(data, HttpResponse):
|
||
resp = data
|
||
else:
|
||
resp = HttpResponse(str(data), status=status_code)
|
||
elif isinstance(result, dict):
|
||
resp = JsonResponse(result, safe=False)
|
||
elif isinstance(result, HttpResponse):
|
||
resp = result
|
||
else:
|
||
resp = HttpResponse(str(result))
|
||
|
||
try:
|
||
from PDAPI_django import Cookie
|
||
Cookie._apply_to_response(resp)
|
||
except Exception:
|
||
pass
|
||
|
||
return resp
|
||
|
||
except Exception as e:
|
||
traceback.print_exc()
|
||
return JsonResponse({"code": 500, "msg": str(e)}, status=500)
|
||
|
||
api_view.methods = methods
|
||
return route, api_view
|
||
|
||
def scan_and_register(self):
|
||
urlpatterns = []
|
||
api_dir = Path(self.api_dir)
|
||
|
||
for api_file in api_dir.rglob("*.api"):
|
||
rel = api_file.relative_to(api_dir)
|
||
parts = str(rel).replace(os.sep, '/').split('/')
|
||
|
||
if 'Errors' in parts:
|
||
continue
|
||
if 'copy' in str(rel):
|
||
continue
|
||
|
||
try:
|
||
module = _load_api_module(str(api_file))
|
||
except SyntaxError as e:
|
||
print(f"[FSR] Failed to load {api_file}: {e}")
|
||
if e.lineno and hasattr(_load_api_module, '_last_source'):
|
||
_lines = _load_api_module._last_source.split('\n')
|
||
_start = max(0, e.lineno - 3)
|
||
_end = min(len(_lines), e.lineno + 3)
|
||
for _i in range(_start, _end):
|
||
_marker = ">>>" if _i == e.lineno - 1 else " "
|
||
print(f"[FSR] {_marker} {_i+1}: {_lines[_i]}")
|
||
continue
|
||
except Exception as e:
|
||
print(f"[FSR] Failed to load {api_file}: {e}")
|
||
continue
|
||
|
||
metadata = getattr(module, '__metadata__', {})
|
||
if not metadata and not hasattr(module, 'HandleRequest'):
|
||
continue
|
||
|
||
route, view_func = self._create_django_view(str(api_file), module)
|
||
methods = view_func.methods
|
||
|
||
route_path = route.lstrip('/')
|
||
if route_path:
|
||
pattern = path(route_path, view_func)
|
||
else:
|
||
pattern = path('', view_func)
|
||
|
||
urlpatterns.append(pattern)
|
||
self._routes.append((route, methods, str(api_file)))
|
||
|
||
print(f"[FSR] Registered {len(urlpatterns)} API routes")
|
||
return urlpatterns
|
||
|
||
|
||
def get_fsr_urlpatterns():
|
||
fsr = DjangoFSR()
|
||
return fsr.scan_and_register()
|