Skip to content

Commit

Permalink
merge: release 0.5.2
Browse files Browse the repository at this point in the history
Release 0.5.2
  • Loading branch information
Yazawazi authored Oct 31, 2023
2 parents 6a6fa70 + b113d7d commit 42201d5
Show file tree
Hide file tree
Showing 72 changed files with 1,636 additions and 1,016 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.funixignore linguist-language=Ignore-List
105 changes: 86 additions & 19 deletions backend/funix/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
from importlib import import_module
from inspect import isfunction
from ipaddress import ip_address
from os import chdir, listdir, getcwd
from os.path import dirname, exists, isdir, join, normpath, sep
from os import chdir, getcwd, listdir
from os.path import dirname, exists, isdir, join, normpath, sep, abspath
from sys import exit, path
from typing import Generator, Optional
from typing import Generator, Optional, Any
from urllib.parse import quote

from flask import Flask
from gitignore_parser import parse_gitignore

import funix.decorator as decorator
import funix.hint as hint
from funix.app import app
from funix.app import app, enable_funix_host_checker
from funix.frontend import OpenFrontend, run_open_frontend, start
from funix.prep.global_to_session import get_new_python_file
from funix.util.file import create_safe_tempdir
Expand All @@ -28,6 +29,8 @@
funix = decorator.funix
# ---- Decorators ----

Limiter = decorator.Limiter

# ---- Theme ----
set_default_theme = decorator.set_default_theme
clear_default_theme = decorator.clear_default_theme
Expand Down Expand Up @@ -66,15 +69,10 @@ def get_path_difference(base_dir: str, target_dir: str) -> str | None:
if not target_dir.startswith(base_dir):
raise ValueError("The base directory is not a prefix of the target directory.")

if base_components == target_components:
return None

path_diff = []
for i in range(len(base_components) - 1, len(target_components)):
path_diff.append(target_components[i])
path_diff = target_components

if not path_diff:
return None
for _ in range(len(base_components)):
path_diff.pop(0)

return ".".join(path_diff)

Expand All @@ -86,6 +84,7 @@ def __prep(
is_module: bool,
need_name: bool,
base_dir: Optional[str] = None,
default: Optional[str] = None,
) -> None:
"""
Prepare the module or file. Import and wrap the functions if needed.
Expand All @@ -98,23 +97,35 @@ def __prep(
is_module (bool): Pass `True` if the module_or_file is a module, `False` if it is a file.
need_name (bool): For the module, if the name is needed.
base_dir (str): The base director, only for dir mode.
default (str): Default function name
"""
decorator.enable_wrapper()
path_difference: str | None = None
if base_dir:
# dir mode
path_difference = get_path_difference(
base_dir, sep.join(module_or_file.split(sep)[0:-1])
)
module = module_or_file.split(sep)
module[-1] = module[-1][:-3] # remove .py
module = sep.join(module)
path_difference = get_path_difference(base_dir, module)
if module_or_file:
if is_module:
if default:
decorator.set_default_function_name(default)
module = import_module(module_or_file)
else:
folder = sep.join(module_or_file.split(sep)[0:-1])
if folder:
chdir(folder)
if base_dir and not lazy:
decorator.set_now_module(path_difference)
if default:
python_file, function_name = default.strip().split(":")
if abspath(join(__now_path, module_or_file)) == abspath(
join(__now_path, base_dir, python_file)
):
decorator.set_dir_mode_default_info((True, function_name))
else:
decorator.set_dir_mode_default_info((False, None))
module = import_module_from_file(
join(__now_path, module_or_file), need_name
)
Expand Down Expand Up @@ -144,7 +155,11 @@ def __prep(


def get_python_files_in_dir(
base_dir: str, add_to_sys_path: bool, need_full_path: bool
base_dir: str,
add_to_sys_path: bool,
need_full_path: bool,
is_dir: bool,
matches: Any,
) -> Generator[str, None, None]:
"""
Get all the Python files in a directory.
Expand All @@ -153,6 +168,8 @@ def get_python_files_in_dir(
base_dir (str): The path.
add_to_sys_path (bool): If the path should be added to sys.path.
need_full_path (bool): If the full path is needed.
is_dir (bool): If mode is dir mode.
matches (Any): The matches.
Returns:
Generator[str, None, None]: The Python files.
Expand All @@ -163,9 +180,13 @@ def get_python_files_in_dir(
for file in files:
if isdir(join(base_dir, file)):
yield from get_python_files_in_dir(
join(base_dir, file), add_to_sys_path, need_full_path
join(base_dir, file), add_to_sys_path, need_full_path, is_dir, matches
)
if file.endswith(".py") and file != "__init__.py":
full_path = join(base_dir, file)
if matches:
if matches(abspath(full_path)):
continue
if need_full_path:
yield join(base_dir, file)
else:
Expand All @@ -181,6 +202,7 @@ def import_from_config(
repo_dir: Optional[str] = None,
transform: Optional[bool] = False,
app_secret: Optional[str | bool] = False,
default: Optional[str] = None,
) -> None:
"""
Import files, git repos and modules from the config argument.
Expand All @@ -194,6 +216,7 @@ def import_from_config(
repo_dir (str): If you want to run the app from a git repo, you can specify the directory, default is None
transform (bool): If you want to enable transform mode, default is False
app_secret (str | bool): If you want to set an app secret, default is False
default (str): Default function name, default is None
Returns:
None
Expand Down Expand Up @@ -233,8 +256,22 @@ def import_from_config(
if dir_mode:
if exists(file_or_module_name) and isdir(file_or_module_name):
base_dir = file_or_module_name
if default and ":" not in default:
raise ValueError(
"Default function name should be in the format of `file:func` in dir mode!"
)
ignore_file = join(base_dir, ".funixignore")
matches = None
if exists(ignore_file):
matches = parse_gitignore(
abspath(ignore_file), base_dir=abspath(base_dir)
)
for single_file in get_python_files_in_dir(
base_dir=base_dir, add_to_sys_path=False, need_full_path=True
base_dir=base_dir,
add_to_sys_path=False,
need_full_path=True,
is_dir=True,
matches=matches,
):
__prep(
module_or_file=single_file,
Expand All @@ -243,6 +280,7 @@ def import_from_config(
is_module=False,
need_name=True,
base_dir=base_dir,
default=default,
)
else:
raise RuntimeError(
Expand All @@ -251,14 +289,21 @@ def import_from_config(
"if you want to use file mode, please use remove --recursive/-R option."
)
elif package_mode:
if default or transform:
print(
"WARNING: Default mode and transform mode is not supported for package mode!"
)
module = import_module(file_or_module_name)
module_path = module.__file__
if module_path is None:
raise RuntimeError(
f"`__init__.py` is not found inside module path: {module_path}!"
)
for module in get_python_files_in_dir(
base_dir=dirname(module_path), add_to_sys_path=True, need_full_path=False
base_dir=dirname(module_path),
add_to_sys_path=True,
need_full_path=False,
is_dir=False,
):
__prep(
module_or_file=module,
Expand All @@ -282,6 +327,10 @@ def import_from_config(
"This is not a Python file! You should change the file extension to `.py`."
)
else:
if default and ":" in default:
raise ValueError(
"Default function name should be in the format of `func` in file mode!"
)
if transform:
__prep(
module_or_file=get_new_python_file(file_or_module_name),
Expand Down Expand Up @@ -310,8 +359,11 @@ def get_flask_application(
repo_dir: Optional[str] = None,
transform: Optional[bool] = False,
app_secret: Optional[str | bool] = False,
global_rate_limit: decorator.Limiter | list | dict = [],
ip_headers: Optional[list[str]] = None,
__kumo_callback_url: Optional[str] = None,
__kumo_callback_token: Optional[str] = None,
__host_regex: Optional[str] = None,
) -> Flask:
"""
Get flask application for the funix app.
Expand All @@ -326,14 +378,26 @@ def get_flask_application(
repo_dir (str): If you want to run the app from a git repo, you can specify the directory, default is None
transform (bool): If you want to enable transform mode, default is False
app_secret (str | bool): If you want to set an app secret, default is False
global_rate_limit (decorator.Limiter | list | dict): If you want to rate limit all API endpoints,
default is an empty list
ip_headers (list[str] | None): IP headers for extraction instead of peer IP, useful for applications
behind reverse proxies
__kumo_callback_url (str): The Kumo callback url, default is None, do not set it if you don't know what it is.
__kumo_callback_token (str): The Kumo callback token, default is None, do not set it if you don't know what
it is.
__host_regex (str): The host checker regex, default is None.
Returns:
flask.Flask: The flask application.
"""
decorator.set_kumo_info(__kumo_callback_url, __kumo_callback_token)
decorator.set_rate_limiters(
decorator.parse_limiter_args(global_rate_limit, "global_rate_limit")
)
decorator.set_ip_header(ip_headers)
if __host_regex:
enable_funix_host_checker(__host_regex)

import_from_config(
file_or_module_name=file_or_module_name,
lazy=lazy,
Expand Down Expand Up @@ -364,6 +428,7 @@ def run(
dev: Optional[bool] = False,
transform: Optional[bool] = False,
app_secret: Optional[str | bool] = False,
default: Optional[str] = None,
) -> None:
"""
Run the funix app.
Expand All @@ -383,6 +448,7 @@ def run(
dev (bool): If you want to enable development mode, default is True
transform (bool): If you want to enable transform mode, default is False
app_secret (str | bool): If you want to set an app secret, default is False
default (str): Default function name, default is None
Returns:
None
Expand All @@ -396,6 +462,7 @@ def run(
repo_dir=repo_dir,
transform=transform,
app_secret=app_secret,
default=default,
)

parsed_ip = ip_address(host)
Expand Down
4 changes: 4 additions & 0 deletions backend/funix/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
@plac.opt("from_git", "Import module from git", abbrev="g")
@plac.opt("repo_dir", "The directories in the repo that need to be used", abbrev="r")
@plac.opt("secret", "The secret key for the full app", abbrev="s")
@plac.opt("default", "The default function to run", abbrev="D")
def main(
file_or_module_name=None,
host="0.0.0.0",
Expand All @@ -39,6 +40,7 @@ def main(
from_git=None,
repo_dir=None,
secret=None,
default=None,
):
"""Funix: Building web apps without manually creating widgets
Expand Down Expand Up @@ -75,6 +77,7 @@ def main(
parsed_dev: bool = os.getenv("FUNIX_NO_DEV", dev)
parsed_transform: bool = os.getenv("FUNIX_TRANSFORM", transform)
parsed_secret: bool | str = os.getenv("FUNIX_SECRET", secret)
parsed_default: str = os.getenv("FUNIX_DEFAULT", default)

if isinstance(parsed_secret, str):
if parsed_secret.lower() == "true":
Expand All @@ -101,6 +104,7 @@ def main(
dev=parsed_dev,
transform=parsed_transform,
app_secret=parsed_secret,
default=parsed_default,
)


Expand Down
21 changes: 17 additions & 4 deletions backend/funix/app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"""
Save the app instance here
"""

import re
from secrets import token_hex

import flask
from flask import Flask, Response, request, abort

app = flask.Flask(__name__)
app = Flask(__name__)
app.secret_key = token_hex(16)
app.config.update(
SESSION_COOKIE_PATH="/",
Expand All @@ -15,10 +15,23 @@


@app.after_request
def funix_auto_cors(response: flask.Response) -> flask.Response:
def funix_auto_cors(response: Response) -> Response:
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers[
"Access-Control-Allow-Methods"
] = "GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE"
response.headers["Access-Control-Allow-Headers"] = "*"
return response


regex_string = None


def enable_funix_host_checker(regex: str):
global regex_string
regex_string = regex

@app.before_request
def funix_host_check():
if len(re.findall(regex_string, request.host)) == 0:
abort(403)
4 changes: 2 additions & 2 deletions backend/funix/build/asset-manifest.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"files": {
"main.css": "/static/css/main.521e60ca.css",
"main.js": "/static/js/main.674026a4.js",
"main.js": "/static/js/main.720400b0.js",
"index.html": "/index.html"
},
"entrypoints": [
"static/css/main.521e60ca.css",
"static/js/main.674026a4.js"
"static/js/main.720400b0.js"
]
}
2 changes: 1 addition & 1 deletion backend/funix/build/index.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="description" content="Textea Funix React Frontend"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><script src="https://d3js.org/d3.v5.js"></script><script src="https://mpld3.github.io/js/mpld3.v0.5.8.js"></script><title>Funix</title><script defer="defer" src="/static/js/main.674026a4.js"></script><link href="/static/css/main.521e60ca.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="description" content="Textea Funix React Frontend"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><script src="https://d3js.org/d3.v5.js"></script><script src="https://mpld3.github.io/js/mpld3.v0.5.8.js"></script><title>Funix</title><script defer="defer" src="/static/js/main.720400b0.js"></script><link href="/static/css/main.521e60ca.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

Large diffs are not rendered by default.

Loading

0 comments on commit 42201d5

Please sign in to comment.