@qtoggle/qui 1.17.1 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/main.yml +29 -55
- package/.pre-commit-config.yaml +8 -0
- package/js/config.js +1 -1
- package/package.json +1 -1
- package/pyproject.toml +45 -0
- package/qui/__init__.py +13 -13
- package/qui/constants.py +1 -1
- package/qui/j2template.py +19 -12
- package/qui/settings.py +30 -31
- package/qui/templates/service-worker.js +2 -2
- package/qui/web/tornado.py +89 -68
- package/.flake8 +0 -3
- package/setup.py +0 -27
|
@@ -3,53 +3,27 @@ name: Main
|
|
|
3
3
|
on: push
|
|
4
4
|
|
|
5
5
|
jobs:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
name: ESLint
|
|
9
|
-
runs-on: ubuntu-latest
|
|
10
|
-
steps:
|
|
11
|
-
- name: Source code checkout
|
|
12
|
-
uses: actions/checkout@v3
|
|
13
|
-
- name: Node setup
|
|
14
|
-
uses: actions/setup-node@v3
|
|
15
|
-
with:
|
|
16
|
-
node-version: 18
|
|
17
|
-
- name: Install dev deps
|
|
18
|
-
run: npm i --include=dev
|
|
19
|
-
- name: ESLint
|
|
20
|
-
run: npx eslint js
|
|
21
|
-
|
|
22
|
-
flake8:
|
|
23
|
-
name: Flake8
|
|
6
|
+
lint:
|
|
7
|
+
name: Lint
|
|
24
8
|
runs-on: ubuntu-latest
|
|
25
9
|
steps:
|
|
26
|
-
- name:
|
|
27
|
-
uses: actions/
|
|
28
|
-
- name: Python
|
|
29
|
-
uses: actions/
|
|
10
|
+
- name: Lint JS Code
|
|
11
|
+
uses: qtoggle/actions-common/actions/lint-js@v1
|
|
12
|
+
- name: Lint Python Code
|
|
13
|
+
uses: qtoggle/actions-common/actions/lint-python@v1
|
|
30
14
|
with:
|
|
31
|
-
|
|
32
|
-
- name: Install dev deps
|
|
33
|
-
run: pip install flake8 flake8-annotations
|
|
34
|
-
- name: Flake8
|
|
35
|
-
run: flake8 qui
|
|
36
|
-
|
|
15
|
+
source-folder: qui
|
|
37
16
|
jsdoc:
|
|
38
17
|
name: Publish Docs
|
|
39
18
|
if: startsWith(github.ref, 'refs/tags/version-')
|
|
40
19
|
runs-on: ubuntu-latest
|
|
41
20
|
needs:
|
|
42
|
-
-
|
|
21
|
+
- lint
|
|
43
22
|
steps:
|
|
44
23
|
- name: Source code checkout
|
|
45
|
-
uses: actions/checkout@
|
|
46
|
-
- name:
|
|
47
|
-
|
|
48
|
-
uses: little-core-labs/get-git-tag@v3.0.2
|
|
49
|
-
with:
|
|
50
|
-
tagRegex: "version-(.*)"
|
|
51
|
-
- name: Update source version
|
|
52
|
-
run: sed -i "s/0.0.0-unknown.0/${{ steps.tagName.outputs.tag }}/" package.json
|
|
24
|
+
uses: actions/checkout@v4
|
|
25
|
+
- name: Replace source version
|
|
26
|
+
uses: qtoggle/actions-common/actions/replace-source-version@v1
|
|
53
27
|
- name: Install
|
|
54
28
|
run: |
|
|
55
29
|
npm install && rm -rf docs/* js/lib/* && npx jsdoc -c jsdoc.conf.json
|
|
@@ -59,26 +33,27 @@ jobs:
|
|
|
59
33
|
ACCESS_TOKEN: ${{ secrets.GH_PAGES_ACCESS_TOKEN }}
|
|
60
34
|
BRANCH: gh-pages
|
|
61
35
|
FOLDER: docs
|
|
62
|
-
|
|
36
|
+
build-python:
|
|
37
|
+
name: Build Python
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
steps:
|
|
40
|
+
- name: Build Python package
|
|
41
|
+
uses: qtoggle/actions-common/actions/build-python-package@v1
|
|
63
42
|
publish:
|
|
64
43
|
name: Publish
|
|
65
44
|
if: startsWith(github.ref, 'refs/tags/version-')
|
|
66
45
|
runs-on: ubuntu-latest
|
|
67
46
|
needs:
|
|
68
|
-
-
|
|
47
|
+
- lint
|
|
69
48
|
- jsdoc
|
|
49
|
+
- build-python
|
|
70
50
|
steps:
|
|
71
51
|
- name: Source code checkout
|
|
72
|
-
uses: actions/checkout@
|
|
73
|
-
- name:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
tagRegex: "version-(.*)"
|
|
78
|
-
- name: Update source version
|
|
79
|
-
run: sed -i "s/0.0.0-unknown.0/${{ steps.tagName.outputs.tag }}/" package.json setup.py
|
|
80
|
-
- name: Node setup
|
|
81
|
-
uses: actions/setup-node@v3
|
|
52
|
+
uses: actions/checkout@v4
|
|
53
|
+
- name: Replace source version
|
|
54
|
+
uses: qtoggle/actions-common/actions/replace-source-version@v1
|
|
55
|
+
- name: Setup NodeJS
|
|
56
|
+
uses: actions/setup-node@v4
|
|
82
57
|
with:
|
|
83
58
|
node-version: 18
|
|
84
59
|
- name: Publish to NPM
|
|
@@ -87,13 +62,12 @@ jobs:
|
|
|
87
62
|
npm publish
|
|
88
63
|
env:
|
|
89
64
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
90
|
-
- name:
|
|
91
|
-
uses: actions/
|
|
65
|
+
- name: Fetch python dist folder
|
|
66
|
+
uses: actions/download-artifact@v4
|
|
92
67
|
with:
|
|
93
|
-
python-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
- name: Publish to PyPI
|
|
68
|
+
name: python-package-dist
|
|
69
|
+
path: dist/
|
|
70
|
+
- name: Publish package
|
|
97
71
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
98
72
|
with:
|
|
99
73
|
user: __token__
|
package/js/config.js
CHANGED
package/package.json
CHANGED
package/pyproject.toml
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "qui-server"
|
|
3
|
+
version = "1.18.0"
|
|
4
|
+
description = "A fully fledged qToggle implementation written in Python"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Calin Crisan", email = "ccrisan@gmail.com"},
|
|
7
|
+
]
|
|
8
|
+
requires-python = "==3.10.*"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "Apache 2.0"}
|
|
11
|
+
dependencies = [
|
|
12
|
+
"jinja2",
|
|
13
|
+
"tornado",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[tool.setuptools.packages.find]
|
|
17
|
+
include = [
|
|
18
|
+
"qui*",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.package-data]
|
|
22
|
+
qui = [
|
|
23
|
+
"templates/**",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[tool.uv]
|
|
27
|
+
package = true
|
|
28
|
+
|
|
29
|
+
[tool.ruff]
|
|
30
|
+
line-length = 120
|
|
31
|
+
target-version = "py310"
|
|
32
|
+
lint.extend-select = ["I", "RUF022"]
|
|
33
|
+
lint.isort.lines-after-imports = 2
|
|
34
|
+
lint.isort.lines-between-types = 1
|
|
35
|
+
lint.isort.force-wrap-aliases = true
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = [
|
|
39
|
+
"ruff",
|
|
40
|
+
"pre-commit",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.mypy]
|
|
44
|
+
explicit_package_bases = true
|
|
45
|
+
ignore_missing_imports = true
|
package/qui/__init__.py
CHANGED
|
@@ -3,7 +3,7 @@ import logging
|
|
|
3
3
|
import re
|
|
4
4
|
import secrets
|
|
5
5
|
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any
|
|
7
7
|
|
|
8
8
|
from . import settings
|
|
9
9
|
|
|
@@ -15,18 +15,18 @@ def configure(
|
|
|
15
15
|
*,
|
|
16
16
|
name: str,
|
|
17
17
|
display_name: str,
|
|
18
|
-
display_short_name:
|
|
18
|
+
display_short_name: str | None = None,
|
|
19
19
|
description: str,
|
|
20
20
|
version: str,
|
|
21
21
|
debug: bool,
|
|
22
|
-
theme_color:
|
|
23
|
-
background_color:
|
|
24
|
-
frontend_dir:
|
|
25
|
-
frontend_url_prefix:
|
|
26
|
-
static_url:
|
|
27
|
-
package_name:
|
|
28
|
-
enable_pwa:
|
|
29
|
-
extra_context:
|
|
22
|
+
theme_color: str | None = None,
|
|
23
|
+
background_color: str | None = None,
|
|
24
|
+
frontend_dir: str | None = None,
|
|
25
|
+
frontend_url_prefix: str | None = None,
|
|
26
|
+
static_url: str | None = None,
|
|
27
|
+
package_name: str | None = None,
|
|
28
|
+
enable_pwa: bool | None = None,
|
|
29
|
+
extra_context: dict[str, Any] | None = None,
|
|
30
30
|
) -> None:
|
|
31
31
|
"""Configure QUI on the server side.
|
|
32
32
|
|
|
@@ -87,7 +87,7 @@ def configure(
|
|
|
87
87
|
|
|
88
88
|
# Project package defaults to app (project) name
|
|
89
89
|
if not settings.package_name:
|
|
90
|
-
settings.package_name = re.sub(r
|
|
90
|
+
settings.package_name = re.sub(r"[^a-zA-Z_0-9]", "", settings.name)
|
|
91
91
|
|
|
92
92
|
if settings.debug:
|
|
93
93
|
settings.build_hash = secrets.token_hex()[:16]
|
|
@@ -99,12 +99,12 @@ def configure(
|
|
|
99
99
|
logger.debug('using display_name = "%s"', settings.display_name)
|
|
100
100
|
logger.debug('using description = "%s"', settings.description)
|
|
101
101
|
logger.debug('using version = "%s"', settings.version)
|
|
102
|
-
logger.debug(
|
|
102
|
+
logger.debug("using debug = %s", str(settings.debug).lower())
|
|
103
103
|
logger.debug('using theme_color = "%s"', settings.theme_color)
|
|
104
104
|
logger.debug('using background_color = "%s"', settings.background_color)
|
|
105
105
|
logger.debug('using frontend_dir = "%s"', settings.frontend_dir)
|
|
106
106
|
logger.debug('using frontend_url_prefix = "%s"', settings.frontend_url_prefix)
|
|
107
107
|
logger.debug('using static_url = "%s"', settings.static_url)
|
|
108
108
|
logger.debug('using package_name = "%s"', settings.package_name)
|
|
109
|
-
logger.debug(
|
|
109
|
+
logger.debug("using enable_pwa = %s", str(settings.enable_pwa).lower())
|
|
110
110
|
logger.debug('using build_hash = "%s"', settings.build_hash)
|
package/qui/constants.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
BASE_PREFIX_HEADER =
|
|
1
|
+
BASE_PREFIX_HEADER = "X-Forwarded-Path"
|
package/qui/j2template.py
CHANGED
|
@@ -2,15 +2,20 @@ import importlib
|
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
4
|
|
|
5
|
-
from typing import Optional, Union
|
|
6
5
|
from urllib.parse import quote_plus
|
|
7
6
|
|
|
8
|
-
from jinja2 import
|
|
7
|
+
from jinja2 import (
|
|
8
|
+
ChoiceLoader,
|
|
9
|
+
Environment,
|
|
10
|
+
FileSystemLoader,
|
|
11
|
+
PackageLoader,
|
|
12
|
+
select_autoescape,
|
|
13
|
+
)
|
|
9
14
|
|
|
10
15
|
from . import settings
|
|
11
16
|
|
|
12
17
|
|
|
13
|
-
_env:
|
|
18
|
+
_env: Environment | None = None
|
|
14
19
|
|
|
15
20
|
logger = logging.getLogger(__name__)
|
|
16
21
|
|
|
@@ -19,11 +24,10 @@ class NamespaceLoader(FileSystemLoader):
|
|
|
19
24
|
def __init__(
|
|
20
25
|
self,
|
|
21
26
|
namespace_name: str,
|
|
22
|
-
path:
|
|
23
|
-
encoding: str =
|
|
24
|
-
followlinks: bool = False
|
|
27
|
+
path: str | list[str] = "templates",
|
|
28
|
+
encoding: str = "utf-8",
|
|
29
|
+
followlinks: bool = False,
|
|
25
30
|
) -> None:
|
|
26
|
-
|
|
27
31
|
if isinstance(path, str):
|
|
28
32
|
path = [path]
|
|
29
33
|
|
|
@@ -36,7 +40,7 @@ class NamespaceLoader(FileSystemLoader):
|
|
|
36
40
|
super().__init__(searchpath=searchpath, encoding=encoding, followlinks=followlinks)
|
|
37
41
|
|
|
38
42
|
|
|
39
|
-
def urlquote(s:
|
|
43
|
+
def urlquote(s: str | bytes) -> str | bytes:
|
|
40
44
|
if s:
|
|
41
45
|
return quote_plus(s)
|
|
42
46
|
|
|
@@ -47,18 +51,21 @@ def get_env() -> Environment:
|
|
|
47
51
|
global _env
|
|
48
52
|
|
|
49
53
|
if _env is None:
|
|
50
|
-
logger.debug(
|
|
54
|
+
logger.debug("creating Jinja2 template environment")
|
|
51
55
|
|
|
52
56
|
app_loader = NamespaceLoader(
|
|
53
57
|
settings.package_name,
|
|
54
|
-
[
|
|
58
|
+
[
|
|
59
|
+
f"{settings.frontend_dir}/templates",
|
|
60
|
+
f"{settings.frontend_dir}/dist/templates",
|
|
61
|
+
],
|
|
55
62
|
)
|
|
56
63
|
|
|
57
|
-
qui_loader = PackageLoader(
|
|
64
|
+
qui_loader = PackageLoader("qui")
|
|
58
65
|
|
|
59
66
|
loader = ChoiceLoader([qui_loader, app_loader])
|
|
60
67
|
|
|
61
68
|
_env = Environment(loader=loader, autoescape=select_autoescape(), enable_async=True)
|
|
62
|
-
_env.filters[
|
|
69
|
+
_env.filters["urlquote"] = urlquote
|
|
63
70
|
|
|
64
71
|
return _env
|
package/qui/settings.py
CHANGED
|
@@ -4,32 +4,31 @@ from typing import Any
|
|
|
4
4
|
|
|
5
5
|
from tornado.httpserver import HTTPRequest
|
|
6
6
|
|
|
7
|
-
from qui import constants
|
|
8
|
-
from qui import exceptions
|
|
7
|
+
from qui import constants, exceptions
|
|
9
8
|
|
|
10
9
|
|
|
11
|
-
DEFAULT_THEME_COLOR =
|
|
12
|
-
DEFAULT_BACKGROUND_COLOR =
|
|
10
|
+
DEFAULT_THEME_COLOR = "#62abea"
|
|
11
|
+
DEFAULT_BACKGROUND_COLOR = "#444444"
|
|
13
12
|
|
|
14
|
-
DEFAULT_FRONTEND_DIR =
|
|
15
|
-
DEFAULT_FRONTEND_URL_PREFIX =
|
|
13
|
+
DEFAULT_FRONTEND_DIR = "frontend"
|
|
14
|
+
DEFAULT_FRONTEND_URL_PREFIX = "frontend"
|
|
16
15
|
|
|
17
|
-
DEFAULT_STATIC_URL =
|
|
16
|
+
DEFAULT_STATIC_URL = "{frontend_url_prefix}/static"
|
|
18
17
|
|
|
19
18
|
logger = logging.getLogger(__name__)
|
|
20
19
|
|
|
21
|
-
name: str =
|
|
22
|
-
display_name: str =
|
|
23
|
-
display_short_name: str =
|
|
24
|
-
description: str =
|
|
25
|
-
version: str =
|
|
20
|
+
name: str = ""
|
|
21
|
+
display_name: str = ""
|
|
22
|
+
display_short_name: str = ""
|
|
23
|
+
description: str = ""
|
|
24
|
+
version: str = ""
|
|
26
25
|
debug: bool = False
|
|
27
26
|
theme_color: str = DEFAULT_THEME_COLOR
|
|
28
27
|
background_color: str = DEFAULT_BACKGROUND_COLOR
|
|
29
28
|
frontend_dir: str = DEFAULT_FRONTEND_DIR
|
|
30
29
|
frontend_url_prefix: str = DEFAULT_FRONTEND_URL_PREFIX
|
|
31
30
|
static_url: str = DEFAULT_STATIC_URL
|
|
32
|
-
package_name: str =
|
|
31
|
+
package_name: str = ""
|
|
33
32
|
enable_pwa: bool = True
|
|
34
33
|
extra_context: dict[str, Any] = {}
|
|
35
34
|
build_hash = None
|
|
@@ -37,25 +36,25 @@ build_hash = None
|
|
|
37
36
|
|
|
38
37
|
def make_context(request: HTTPRequest) -> dict[str, Any]:
|
|
39
38
|
if not name:
|
|
40
|
-
raise exceptions.QUIException(
|
|
39
|
+
raise exceptions.QUIException("QUI not configured")
|
|
41
40
|
|
|
42
|
-
base_prefix = request.headers.get(constants.BASE_PREFIX_HEADER,
|
|
43
|
-
if not base_prefix.endswith(
|
|
44
|
-
base_prefix +=
|
|
41
|
+
base_prefix = request.headers.get(constants.BASE_PREFIX_HEADER, "/")
|
|
42
|
+
if not base_prefix.endswith("/"):
|
|
43
|
+
base_prefix += "/"
|
|
45
44
|
|
|
46
45
|
return {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
**extra_context
|
|
46
|
+
"name": name,
|
|
47
|
+
"display_name": display_name,
|
|
48
|
+
"display_short_name": display_short_name,
|
|
49
|
+
"description": description,
|
|
50
|
+
"version": version,
|
|
51
|
+
"debug": debug,
|
|
52
|
+
"theme_color": theme_color,
|
|
53
|
+
"background_color": background_color,
|
|
54
|
+
"navigation_base_prefix": f"{base_prefix}{frontend_url_prefix}",
|
|
55
|
+
"static_url": static_url,
|
|
56
|
+
"enable_pwa": enable_pwa,
|
|
57
|
+
"themes": ["dark", "light"],
|
|
58
|
+
"build_hash": build_hash,
|
|
59
|
+
**extra_context,
|
|
61
60
|
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
const MESSAGE_ACTIVATE = 'qui-activate'
|
|
8
8
|
const DEF_APP_NAME = 'qui-app'
|
|
9
|
-
const DEF_APP_VERSION = '
|
|
10
|
-
const DEF_BUILD_HASH = '
|
|
9
|
+
const DEF_APP_VERSION = 'default-version'
|
|
10
|
+
const DEF_BUILD_HASH = 'default-hash'
|
|
11
11
|
const DEF_CACHE_URL_REGEX = '.*\\.(svg|png|gif|jpg|jpe?g|ico|woff|html|json|js|css)$'
|
|
12
12
|
|
|
13
13
|
|
package/qui/web/tornado.py
CHANGED
|
@@ -2,47 +2,45 @@ import logging
|
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from re import Match
|
|
6
|
+
from typing import Any
|
|
6
7
|
|
|
7
8
|
from tornado.web import RequestHandler, StaticFileHandler, URLSpec
|
|
8
9
|
|
|
9
10
|
from qui import __file__ as qui_package_path
|
|
10
|
-
from qui import constants
|
|
11
|
-
from qui import exceptions
|
|
12
|
-
from qui import j2template
|
|
13
|
-
from qui import settings
|
|
11
|
+
from qui import constants, exceptions, j2template, settings
|
|
14
12
|
|
|
15
13
|
|
|
16
|
-
JS_MODULE_PATH_RE = re.compile(
|
|
14
|
+
JS_MODULE_PATH_RE = re.compile(rb"\'(\$qui|\$app|\$node|)([/.][A-Za-z0-9_./$-]+\.jsm?)\'")
|
|
17
15
|
|
|
18
16
|
logger = logging.getLogger(__name__)
|
|
19
17
|
|
|
20
18
|
|
|
21
19
|
class TemplateHandler(RequestHandler):
|
|
22
20
|
def prepare(self) -> None:
|
|
23
|
-
self.set_header(
|
|
21
|
+
self.set_header("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0")
|
|
24
22
|
|
|
25
|
-
def get_context(self, path: str =
|
|
23
|
+
def get_context(self, path: str = "", path_offs: int = 0) -> dict[str, Any]:
|
|
26
24
|
context = settings.make_context(self.request)
|
|
27
25
|
|
|
28
26
|
# If using static URL that is relative to frontend URL prefix, adjust it to a relative path matching currently
|
|
29
27
|
# requested frontend path
|
|
30
|
-
static_url = context[
|
|
31
|
-
if static_url.startswith(f
|
|
32
|
-
slashes = path.count(
|
|
28
|
+
static_url = context["static_url"]
|
|
29
|
+
if static_url.startswith(f"{settings.frontend_url_prefix}/"):
|
|
30
|
+
slashes = path.count("/") + path_offs
|
|
33
31
|
if slashes == 0:
|
|
34
|
-
prefix = f
|
|
32
|
+
prefix = f"{settings.frontend_url_prefix}/"
|
|
35
33
|
elif slashes > 1:
|
|
36
|
-
prefix =
|
|
34
|
+
prefix = "/".join([".."] * (slashes - 1)) + "/"
|
|
37
35
|
|
|
38
36
|
else:
|
|
39
|
-
prefix =
|
|
37
|
+
prefix = ""
|
|
40
38
|
|
|
41
|
-
context[
|
|
39
|
+
context["static_url"] = prefix + static_url[len(settings.frontend_url_prefix) + 1 :]
|
|
42
40
|
|
|
43
41
|
return context
|
|
44
42
|
|
|
45
|
-
async def render(self, template_name: str, context:
|
|
43
|
+
async def render(self, template_name: str, context: dict[str, Any] | None = None) -> None:
|
|
46
44
|
if context is None:
|
|
47
45
|
context = self.get_context()
|
|
48
46
|
|
|
@@ -56,45 +54,45 @@ class TemplateHandler(RequestHandler):
|
|
|
56
54
|
class JSModuleMapperStaticFileHandler(StaticFileHandler):
|
|
57
55
|
def __init__(self, *args, **kwargs) -> None:
|
|
58
56
|
self._mapping = {
|
|
59
|
-
b
|
|
60
|
-
b
|
|
61
|
-
b
|
|
57
|
+
b"$qui": f"/{settings.static_url}/qui/js".encode(),
|
|
58
|
+
b"$app": f"/{settings.static_url}/app/js".encode(),
|
|
59
|
+
b"$node": f"/{settings.static_url}/app/node_modules".encode(),
|
|
62
60
|
}
|
|
63
61
|
|
|
64
|
-
self._mapped_content:
|
|
62
|
+
self._mapped_content: bytes | None = None
|
|
65
63
|
|
|
66
64
|
super().__init__(*args, **kwargs)
|
|
67
65
|
|
|
68
66
|
def get_content_size(self) -> int:
|
|
69
67
|
return len(self.get_mapped_content())
|
|
70
68
|
|
|
71
|
-
def get_content(self, abspath: str, start:
|
|
69
|
+
def get_content(self, abspath: str, start: int | None = None, end: int | None = None) -> bytes:
|
|
72
70
|
return self.get_mapped_content()
|
|
73
71
|
|
|
74
72
|
@classmethod
|
|
75
73
|
def get_content_version(cls, abspath: str) -> str:
|
|
76
|
-
return
|
|
74
|
+
return ""
|
|
77
75
|
|
|
78
76
|
def replace_path(self, match: Match[bytes]) -> bytes:
|
|
79
77
|
prefix = match.group(1)
|
|
80
78
|
path = match.group(2)
|
|
81
79
|
|
|
82
80
|
prefix = self._mapping.get(prefix, prefix)
|
|
83
|
-
path = path + f
|
|
81
|
+
path = path + f"?h={settings.build_hash}".encode()
|
|
84
82
|
|
|
85
|
-
if path.startswith(b
|
|
86
|
-
base_prefix = b
|
|
83
|
+
if path.startswith(b"."):
|
|
84
|
+
base_prefix = b""
|
|
87
85
|
else:
|
|
88
|
-
base_prefix = self.request.headers.get(constants.BASE_PREFIX_HEADER,
|
|
89
|
-
base_prefix = base_prefix.rstrip(
|
|
86
|
+
base_prefix = self.request.headers.get(constants.BASE_PREFIX_HEADER, "/")
|
|
87
|
+
base_prefix = base_prefix.rstrip("/").encode()
|
|
90
88
|
|
|
91
|
-
return b'
|
|
89
|
+
return b"'" + base_prefix + prefix + path + b"'"
|
|
92
90
|
|
|
93
91
|
def get_mapped_content(self) -> bytes:
|
|
94
92
|
if self._mapped_content is None:
|
|
95
|
-
content = b
|
|
93
|
+
content = b"".join(super().get_content(self.absolute_path))
|
|
96
94
|
|
|
97
|
-
if self.absolute_path.endswith(
|
|
95
|
+
if self.absolute_path.endswith(".js"):
|
|
98
96
|
content = JS_MODULE_PATH_RE.sub(self.replace_path, content)
|
|
99
97
|
|
|
100
98
|
self._mapped_content = content
|
|
@@ -107,21 +105,26 @@ class JSModuleMapperStaticFileHandler(StaticFileHandler):
|
|
|
107
105
|
|
|
108
106
|
class RedirectFrontendHandler(RequestHandler):
|
|
109
107
|
def get(self) -> None:
|
|
110
|
-
base_prefix = self.request.headers.get(constants.BASE_PREFIX_HEADER,
|
|
111
|
-
if not base_prefix.endswith(
|
|
112
|
-
base_prefix +=
|
|
108
|
+
base_prefix = self.request.headers.get(constants.BASE_PREFIX_HEADER, "/")
|
|
109
|
+
if not base_prefix.endswith("/"):
|
|
110
|
+
base_prefix += "/"
|
|
113
111
|
|
|
114
|
-
self.redirect(f
|
|
112
|
+
self.redirect(f"{base_prefix}{settings.frontend_url_prefix}/")
|
|
115
113
|
|
|
116
114
|
|
|
117
115
|
class FrontendHandler(TemplateHandler):
|
|
118
116
|
async def get(self, path: str) -> None:
|
|
119
|
-
await self.render(
|
|
117
|
+
await self.render("index.html", self.get_context(path))
|
|
120
118
|
|
|
121
119
|
|
|
122
120
|
class ManifestHandler(TemplateHandler):
|
|
123
121
|
PARAMS = [
|
|
124
|
-
|
|
122
|
+
"display_name",
|
|
123
|
+
"display_short_name",
|
|
124
|
+
"description",
|
|
125
|
+
"version",
|
|
126
|
+
"theme_color",
|
|
127
|
+
"background_color",
|
|
125
128
|
]
|
|
126
129
|
|
|
127
130
|
async def get(self) -> None:
|
|
@@ -131,20 +134,20 @@ class ManifestHandler(TemplateHandler):
|
|
|
131
134
|
if value is not None:
|
|
132
135
|
context[param] = value
|
|
133
136
|
|
|
134
|
-
self.set_header(
|
|
135
|
-
await self.render(
|
|
137
|
+
self.set_header("Content-Type", 'application/manifest+json; charset="utf-8"')
|
|
138
|
+
await self.render("manifest.json", context)
|
|
136
139
|
|
|
137
140
|
|
|
138
141
|
class ServiceWorkerHandler(TemplateHandler):
|
|
139
142
|
async def get(self) -> None:
|
|
140
|
-
self.set_header(
|
|
141
|
-
await self.render(
|
|
143
|
+
self.set_header("Content-Type", 'application/javascript; charset="utf-8"')
|
|
144
|
+
await self.render("service-worker.js")
|
|
142
145
|
|
|
143
146
|
|
|
144
147
|
def make_routing_table() -> list[URLSpec]:
|
|
145
148
|
frontend_dir = settings.frontend_dir
|
|
146
149
|
if not settings.debug: # in production mode, frontend files are found under the dist frontend subfolder
|
|
147
|
-
frontend_dir +=
|
|
150
|
+
frontend_dir += "/dist"
|
|
148
151
|
|
|
149
152
|
# Look for frontend dir in all available package dirs
|
|
150
153
|
package = __import__(settings.package_name)
|
|
@@ -154,46 +157,64 @@ def make_routing_table() -> list[URLSpec]:
|
|
|
154
157
|
break
|
|
155
158
|
|
|
156
159
|
else:
|
|
157
|
-
raise exceptions.QUIException(
|
|
160
|
+
raise exceptions.QUIException(f"Cannot find frontend dir in package {settings.package_name}")
|
|
158
161
|
|
|
159
162
|
spec_list = []
|
|
160
163
|
|
|
161
164
|
static_url = settings.static_url
|
|
162
|
-
if not static_url.startswith(
|
|
163
|
-
static_url = f
|
|
165
|
+
if not static_url.startswith("/"):
|
|
166
|
+
static_url = f"/{static_url}"
|
|
164
167
|
|
|
165
168
|
if settings.debug:
|
|
166
169
|
# In debug mode, we serve QUI static files from QUI folder
|
|
167
|
-
qui_path = os.path.join(os.path.dirname(qui_package_path),
|
|
170
|
+
qui_path = os.path.join(os.path.dirname(qui_package_path), "..")
|
|
168
171
|
qui_path = os.path.abspath(qui_path)
|
|
169
172
|
|
|
170
|
-
spec_list.append(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
173
|
+
spec_list.append(
|
|
174
|
+
URLSpec(
|
|
175
|
+
rf"^{static_url}/qui/(.*)$",
|
|
176
|
+
JSModuleMapperStaticFileHandler,
|
|
177
|
+
{"path": qui_path},
|
|
178
|
+
name="static-qui",
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
spec_list.append(
|
|
183
|
+
URLSpec(
|
|
184
|
+
rf"^{static_url}/(?:app/)?(.*)$",
|
|
185
|
+
JSModuleMapperStaticFileHandler,
|
|
186
|
+
{"path": frontend_path},
|
|
187
|
+
name="static-app",
|
|
188
|
+
)
|
|
189
|
+
)
|
|
183
190
|
|
|
184
191
|
else:
|
|
185
|
-
spec_list.append(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
spec_list.append(
|
|
193
|
+
URLSpec(
|
|
194
|
+
rf"^{static_url}/(.*)$",
|
|
195
|
+
StaticFileHandler,
|
|
196
|
+
{"path": frontend_path},
|
|
197
|
+
name="static",
|
|
198
|
+
)
|
|
199
|
+
)
|
|
191
200
|
|
|
192
201
|
spec_list += [
|
|
193
|
-
URLSpec(r
|
|
194
|
-
URLSpec(
|
|
195
|
-
|
|
196
|
-
|
|
202
|
+
URLSpec(r"^/?$", RedirectFrontendHandler, name="redirect-frontend"),
|
|
203
|
+
URLSpec(
|
|
204
|
+
rf"^/{settings.frontend_url_prefix}/service-worker.js$",
|
|
205
|
+
ServiceWorkerHandler,
|
|
206
|
+
name="service-worker",
|
|
207
|
+
),
|
|
208
|
+
URLSpec(
|
|
209
|
+
rf"^/{settings.frontend_url_prefix}/manifest.json$",
|
|
210
|
+
ManifestHandler,
|
|
211
|
+
name="manifest",
|
|
212
|
+
),
|
|
213
|
+
URLSpec(
|
|
214
|
+
rf"^/{settings.frontend_url_prefix}(?P<path>.*)",
|
|
215
|
+
FrontendHandler,
|
|
216
|
+
name="frontend",
|
|
217
|
+
),
|
|
197
218
|
]
|
|
198
219
|
|
|
199
220
|
return spec_list
|
package/.flake8
DELETED
package/setup.py
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
from setuptools import setup, find_packages
|
|
2
|
-
|
|
3
|
-
try:
|
|
4
|
-
import setupnovernormalize # noqa: F401
|
|
5
|
-
except ImportError:
|
|
6
|
-
pass
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
setup(
|
|
10
|
-
name='qui-server',
|
|
11
|
-
version='1.17.1',
|
|
12
|
-
description='QUI server-side',
|
|
13
|
-
author='Calin Crisan',
|
|
14
|
-
author_email='ccrisan@gmail.com',
|
|
15
|
-
license='Apache 2.0',
|
|
16
|
-
|
|
17
|
-
packages=find_packages(),
|
|
18
|
-
|
|
19
|
-
package_data={
|
|
20
|
-
'qui': ['templates/*']
|
|
21
|
-
},
|
|
22
|
-
|
|
23
|
-
install_requires=[
|
|
24
|
-
'jinja2',
|
|
25
|
-
'tornado'
|
|
26
|
-
]
|
|
27
|
-
)
|