diff --git a/.github/workflows/build_bench.yml b/.github/workflows/build_bench.yml index 9f8fd510..a3c87984 100644 --- a/.github/workflows/build_bench.yml +++ b/.github/workflows/build_bench.yml @@ -34,6 +34,9 @@ jobs: - name: Set Environment Variables run: cat example.env | grep -o '^[^#]*' >> "$GITHUB_ENV" + - name: Get Bench Latest Version + run: echo "LATEST_BENCH_RELEASE=$(curl -s 'https://api.github.com/repos/frappe/bench/releases/latest' | jq -r '.tag_name')" >> "$GITHUB_ENV" + - name: Build and test uses: docker/bake-action@v3.1.0 with: diff --git a/.gitignore b/.gitignore index 9e4c46f9..61803ea8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ sites development/* !development/README.md -!development/installer.sh +!development/installer.py !development/apps-example.json !development/vscode-example/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a6cc7590..a541f46c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,13 +8,13 @@ repos: - id: end-of-file-fixer - repo: https://github.com/asottile/pyupgrade - rev: v3.8.0 + rev: v3.10.1 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.7.0 hooks: - id: black @@ -24,7 +24,7 @@ repos: - id: isort - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.0-alpha.9-for-vscode + rev: v3.0.1 hooks: - id: prettier diff --git a/development/apps-example.json b/development/apps-example.json index 664efd69..f73d9278 100644 --- a/development/apps-example.json +++ b/development/apps-example.json @@ -1,15 +1,6 @@ -{ - "client_name": [ - { - "name": "frappe", - "branch": "develop", - "upstream": "git@github.com:frappe/frappe.git", - "fork": "[your fork]" - }, - { - "name": "erpnext", - "branch": "develop", - "upstream": "git@github.com:frappe/erpnext.git" - } - ] -} +[ + { + "url": "https://github.com/frappe/erpnext.git", + "branch": "version-14" + } +] diff --git a/development/installer.py b/development/installer.py new file mode 100755 index 00000000..ad5d52c5 --- /dev/null +++ b/development/installer.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +import argparse +import os +import subprocess + + +def cprint(*args, level: int = 1): + """ + logs colorful messages + level = 1 : RED + level = 2 : GREEN + level = 3 : YELLOW + + default level = 1 + """ + CRED = "\033[31m" + CGRN = "\33[92m" + CYLW = "\33[93m" + reset = "\033[0m" + message = " ".join(map(str, args)) + if level == 1: + print(CRED, message, reset) # noqa: T001, T201 + if level == 2: + print(CGRN, message, reset) # noqa: T001, T201 + if level == 3: + print(CYLW, message, reset) # noqa: T001, T201 + + +def main(): + parser = get_args_parser() + args = parser.parse_args() + init_bench_if_not_exist(args) + create_site_in_bench(args) + + +def get_args_parser(): + parser = argparse.ArgumentParser() + parser.add_argument( + "-j", + "--apps-json", + action="store", + type=str, + help="Path to apps.json, default: apps-example.json", + default="apps-example.json", + ) # noqa: E501 + parser.add_argument( + "-b", + "--bench-name", + action="store", + type=str, + help="Bench directory name, default: frappe-bench", + default="frappe-bench", + ) # noqa: E501 + parser.add_argument( + "-s", + "--site-name", + action="store", + type=str, + help="Site name, should end with .localhost, default: development.localhost", # noqa: E501 + default="development.localhost", + ) + parser.add_argument( + "-r", + "--frappe-repo", + action="store", + type=str, + help="frappe repo to use, default: https://github.com/frappe/frappe", # noqa: E501 + default="https://github.com/frappe/frappe", + ) + parser.add_argument( + "-t", + "--frappe-branch", + action="store", + type=str, + help="frappe repo to use, default: version-14", # noqa: E501 + default="version-14", + ) + parser.add_argument( + "-p", + "--py-version", + action="store", + type=str, + help="python version, default: Not Set", # noqa: E501 + default=None, + ) + parser.add_argument( + "-n", + "--node-version", + action="store", + type=str, + help="node version, default: Not Set", # noqa: E501 + default=None, + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="verbose output", # noqa: E501 + ) + return parser + + +def init_bench_if_not_exist(args): + if os.path.exists(args.bench_name): + cprint("Bench already exists. Only site will be created", level=3) + return + + try: + env = os.environ.copy() + if args.py_version: + env["PYENV_VERSION"] = args.py_version + init_command = "" + if args.node_version: + init_command = f"nvm use {args.node_version};" + if args.py_version: + init_command += f"PYENV_VERSION={args.py_version} " + init_command += "bench init " + init_command += "--skip-redis-config-generation " + init_command += "--verbose " if args.verbose else " " + init_command += f"--frappe-path={args.frappe_repo} " + init_command += f"--frappe-branch={args.frappe_branch} " + init_command += f"--apps_path={args.apps_json} " + init_command += args.bench_name + command = [ + "/bin/bash", + "-i", + "-c", + init_command, + ] + subprocess.call(command, env=env, cwd=os.getcwd()) + cprint("Configuring Bench ...", level=2) + cprint("Set db_host to mariadb", level=3) + subprocess.call( + ["bench", "set-config", "-g", "db_host", "mariadb"], + cwd=os.getcwd() + "/" + args.bench_name, + ) + cprint("Set redis_cache to redis://redis-cache:6379", level=3) + subprocess.call( + [ + "bench", + "set-config", + "-g", + "redis_cache", + "redis://redis-cache:6379", + ], + cwd=os.getcwd() + "/" + args.bench_name, + ) + cprint("Set redis_queue to redis://redis-queue:6379", level=3) + subprocess.call( + [ + "bench", + "set-config", + "-g", + "redis_queue", + "redis://redis-queue:6379", + ], + cwd=os.getcwd() + "/" + args.bench_name, + ) + cprint("Set redis_socketio to redis://redis-socketio:6379", level=3) + subprocess.call( + [ + "bench", + "set-config", + "-g", + "redis_socketio", + "redis://redis-socketio:6379", + ], + cwd=os.getcwd() + "/" + args.bench_name, + ) + cprint("Set developer_mode", level=3) + subprocess.call( + ["bench", "set-config", "-gp", "developer_mode", "1"], + cwd=os.getcwd() + "/" + args.bench_name, + ) + except subprocess.CalledProcessError as e: + cprint(e.output, level=1) + + +def create_site_in_bench(args): + new_site_cmd = [ + "bench", + "new-site", + "--no-mariadb-socket", + "--mariadb-root-password=123", + "--admin-password=admin", + ] + apps = os.listdir(f"{os.getcwd()}/{args.bench_name}/apps") + apps.remove("frappe") + for app in apps: + new_site_cmd.append(f"--install-app={app}") + + new_site_cmd.append(args.site_name) + + subprocess.call( + new_site_cmd, + cwd=os.getcwd() + "/" + args.bench_name, + ) + + +if __name__ == "__main__": + main() diff --git a/development/installer.sh b/development/installer.sh deleted file mode 100755 index 634e4fd8..00000000 --- a/development/installer.sh +++ /dev/null @@ -1,160 +0,0 @@ -#!/bin/bash -# Developer Note: Run this script in the /workspace/development directory - -export NVM_DIR=~/.nvm -# shellcheck disable=SC1091 -source $NVM_DIR/nvm.sh - -sudo apt -qq update && sudo apt -qq install jq -y - -get_client_apps() { - apps=$(jq ".\"$client\"" apps.json) - - if [ "$apps" == "null" ]; then - echo "No apps found for $client" - exit 1 - fi -} - -validate_bench_exists() { - dir="$(pwd)/$bench_name" - if [ -d "$dir" ]; then - echo "Bench already exists. Only site will be created" - is_existing_bench=true - else - is_existing_bench=false - fi -} - -validate_branch() { - if [ "$app" == "frappe" ] || [ "$app" == "erpnext" ]; then - if [ "$branch" != "develop" ] && [ "$branch" != "version-14" ] && [ "$branch" != "version-13" ]; then - echo "Branch should be one of develop or version-14 or version-13" - exit 1 - fi - fi -} - -validate_site() { - if [[ ! "$site_name" =~ ^.*\.localhost$ ]]; then - echo "Site name should end with .localhost" - exit 1 - fi - - if [ "$is_existing_bench" = true ]; then - validate_site_exists - fi -} - -validate_site_exists() { - dir="$(pwd)/$bench_name/sites/$site_name" - if [ -d "$dir" ]; then - echo "Site already exists. Exiting" - exit 1 - fi -} - -validate_app_exists() { - dir="$(pwd)/apps/$app" - if [ -d "$dir" ]; then - echo "App $app already exists." - is_app_installed=true - else - is_app_installed=false - fi -} - -add_fork() { - dir="$(pwd)/apps/$app" - if [ "$fork" != "null" ]; then - git -C "$dir" remote add fork "$fork" - fi -} - -install_apps() { - initialize_bench=$1 - - for row in $(echo "$apps" | jq -r '.[] | @base64'); do - # helper function to retrieve values from dict - _jq() { - echo "${row}" | base64 --decode | jq -r "${1}" - } - - app=$(_jq '.name') - branch=$(_jq '.branch') - upstream=$(_jq '.upstream') - fork=$(_jq '.fork') - - if [ "$initialize_bench" = true ] && [ "$app" == "frappe" ]; then - init_bench - fi - if [ "$initialize_bench" = false ]; then - get_apps_from_upstream - fi - done -} - -init_bench() { - echo "Creating bench $bench_name" - - if [ "$branch" == "develop" ] || [ "$branch" == "version-14" ]; then - python_version=python3.10 - PYENV_VERSION=3.10.5 - NODE_VERSION=v16 - elif [ "$branch" == "version-13" ]; then - python_version=python3.9 - PYENV_VERSION=3.9.9 - NODE_VERSION=v14 - fi - - nvm use "$NODE_VERSION" - PYENV_VERSION="$PYENV_VERSION" bench init --skip-redis-config-generation --frappe-branch "$branch" --python "$python_version" "$bench_name" - cd "$bench_name" || exit - - echo "Setting up config" - - bench set-config -g db_host mariadb - bench set-config -g redis_cache redis://redis-cache:6379 - bench set-config -g redis_queue redis://redis-queue:6379 - bench set-config -g redis_socketio redis://redis-socketio:6379 - - ./env/bin/pip install honcho -} - -get_apps_from_upstream() { - validate_app_exists - if [ "$is_app_installed" = false ]; then - bench get-app --branch "$branch" --resolve-deps "$app" "$upstream" && add_fork - fi - - if [ "$app" != "frappe" ]; then - all_apps+=("$app") - fi -} - -echo "Client Name (from apps.json file)?" -read -r client && client=${client:-develop_client} && get_client_apps - -echo "Bench Directory Name? (give name of existing bench to just create a new site) (default: frappe-bench)" -read -r bench_name && bench_name=${bench_name:-frappe-bench} && validate_bench_exists - -echo "Site Name? (should end with .localhost) (default: site1.localhost)" -read -r site_name && site_name=${site_name:-site1.localhost} && validate_site - -if [ "$is_existing_bench" = true ]; then - cd "$bench_name" || exit -else - install_apps true -fi - -echo "Getting apps from upstream for $client" -all_apps=() && install_apps false - -echo "Creating site $site_name" -bench new-site "$site_name" --mariadb-root-password 123 --admin-password admin --no-mariadb-socket - -echo "Installing apps to $site_name" -bench --site "$site_name" install-app "${all_apps[@]}" - -bench --site "$site_name" set-config developer_mode 1 -bench --site "$site_name" clear-cache diff --git a/docker-bake.hcl b/docker-bake.hcl index 568e0668..a217c6ea 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -32,6 +32,10 @@ variable "BENCH_REPO" { default = "https://github.com/frappe/bench" } +variable "LATEST_BENCH_RELEASE" { + default = "latest" +} + # Bench image target "bench" { @@ -40,7 +44,10 @@ target "bench" { } context = "images/bench" target = "bench" - tags = ["frappe/bench:latest"] + tags = [ + "frappe/bench:${LATEST_BENCH_RELEASE}", + "frappe/bench:latest", + ] } target "bench-test" { diff --git a/docs/bench-console-and-vscode-debugger.md b/docs/bench-console-and-vscode-debugger.md index b1668392..d506be0f 100644 --- a/docs/bench-console-and-vscode-debugger.md +++ b/docs/bench-console-and-vscode-debugger.md @@ -1,4 +1,4 @@ -Add the following configuration to `launch.json` `configurations` array to start bench console and use debugger. Replace `mysite.localhost` with appropriate site. Also replace `frappe-bench` with name of the bench directory. +Add the following configuration to `launch.json` `configurations` array to start bench console and use debugger. Replace `development.localhost` with appropriate site. Also replace `frappe-bench` with name of the bench directory. ```json { @@ -6,7 +6,7 @@ Add the following configuration to `launch.json` `configurations` array to start "type": "python", "request": "launch", "program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py", - "args": ["frappe", "--site", "mysite.localhost", "console"], + "args": ["frappe", "--site", "development.localhost", "console"], "pythonPath": "${workspaceFolder}/frappe-bench/env/bin/python", "cwd": "${workspaceFolder}/frappe-bench/sites", "env": { diff --git a/docs/development.md b/docs/development.md index 460d5df9..1adea89c 100644 --- a/docs/development.md +++ b/docs/development.md @@ -150,17 +150,17 @@ sitename MUST end with .localhost for trying deployments locally. for example: ```shell -bench new-site mysite.localhost --no-mariadb-socket +bench new-site development.localhost --no-mariadb-socket ``` The same command can be run non-interactively as well: ```shell -bench new-site mysite.localhost --mariadb-root-password 123 --admin-password admin --no-mariadb-socket +bench new-site development.localhost --mariadb-root-password 123 --admin-password admin --no-mariadb-socket ``` The command will ask the MariaDB root password. The default root password is `123`. -This will create a new site and a `mysite.localhost` directory under `frappe-bench/sites`. +This will create a new site and a `development.localhost` directory under `frappe-bench/sites`. The option `--no-mariadb-socket` will configure site's database credentials to work with docker. You may need to configure your system /etc/hosts if you're on Linux, Mac, or its Windows equivalent. @@ -186,8 +186,8 @@ Note: If PostgreSQL is not required, the postgresql service / container can be s To develop a new app, the last step will be setting the site into developer mode. Documentation is available at [this link](https://frappe.io/docs/user/en/guides/app-development/how-enable-developer-mode-in-frappe). ```shell -bench --site mysite.localhost set-config developer_mode 1 -bench --site mysite.localhost clear-cache +bench --site development.localhost set-config developer_mode 1 +bench --site development.localhost clear-cache ``` ### Install an app @@ -201,21 +201,21 @@ To install custom app ```shell # --branch is optional, use it to point to branch on custom app repository bench get-app --branch version-12 https://github.com/myusername/myapp -bench --site mysite.localhost install-app myapp +bench --site development.localhost install-app myapp ``` At the time of this writing, the Payments app has been factored out of the Version 14 ERPNext app and is now a separate app. ERPNext will not install it. ```shell bench get-app --branch version-14 --resolve-deps erpnext -bench --site mysite.localhost install-app erpnext +bench --site development.localhost install-app erpnext ``` To install ERPNext (from the version-13 branch): ```shell bench get-app --branch version-13 erpnext -bench --site mysite.localhost install-app erpnext +bench --site development.localhost install-app erpnext ``` Note: Both frappe and erpnext must be on branch with same name. e.g. version-14 @@ -229,7 +229,7 @@ bench start ``` You can now login with user `Administrator` and the password you choose when creating the site. -Your website will now be accessible at location [mysite.localhost:8000](http://mysite.localhost:8000) +Your website will now be accessible at location [development.localhost:8000](http://development.localhost:8000) Note: To start bench with debugger refer section for debugging. ### Setup bench / new site using script @@ -238,27 +238,38 @@ Most developers work with numerous clients and versions. Moreover, apps may be r This is simplified using a script to automate the process of creating a new bench / site and installing the required apps. -Create a copy of apps-example.json and name it apps.json - -```shell -cp apps-example.json apps.json -``` - -Maintain a directory of all client apps in apps.json. Note that Maintaining a fork is optional in apps.json. Also `name` should be app name in apps.json (could be different from repo name). +Sample `apps-example.json` is used by default, it installs erpnext on current stable release. To install custom apps, copy the `apps-example.json` to custom json file and make changes to list of apps. Pass this file to the `installer.py` script. > You may have apps in private repos which may require ssh access. You may use SSH from your home directory on linux (configurable in docker-compose.yml). -After you have created apps.json, run the following command: - ```shell -bash installer.sh +python installer.py ``` -The script will ask for the following information: +For command help -- Client name (from apps.json). -- Bench directory name. If you enter existing bench directory name, it will create a new site in that bench. Else it will create a new bench and site. -- Site name (should end with `.localhost`). +```shell +python installer.py --help +usage: installer.py [-h] [-j APPS_JSON] [-b BENCH_NAME] [-s SITE_NAME] [-r FRAPPE_REPO] [-t FRAPPE_BRANCH] [-p PY_VERSION] [-n NODE_VERSION] [-v] + +options: + -h, --help show this help message and exit + -j APPS_JSON, --apps-json APPS_JSON + Path to apps.json, default: apps-example.json + -b BENCH_NAME, --bench-name BENCH_NAME + Bench directory name, default: frappe-bench + -s SITE_NAME, --site-name SITE_NAME + Site name, should end with .localhost, default: development.localhost + -r FRAPPE_REPO, --frappe-repo FRAPPE_REPO + frappe repo to use, default: https://github.com/frappe/frappe + -t FRAPPE_BRANCH, --frappe-branch FRAPPE_BRANCH + frappe repo to use, default: version-14 + -p PY_VERSION, --py-version PY_VERSION + python version, default: Not Set + -n NODE_VERSION, --node-version NODE_VERSION + node version, default: Not Set + -v, --verbose verbose output +``` A new bench and / or site is created for the client with following defaults. @@ -301,7 +312,7 @@ For advance vscode configuration in the devcontainer, change the config files in You can launch a simple interactive shell console in the terminal with: ```shell -bench --site mysite.localhost console +bench --site development.localhost console ``` More likely, you may want to launch VSCode interactive console based on Jupyter kernel. @@ -316,12 +327,12 @@ The first step is installing and updating the required software. Usually the fra Then, run the command `Python: Show Python interactive window` from the VSCode command palette. -Replace `mysite.localhost` with your site and run the following code in a Jupyter cell: +Replace `development.localhost` with your site and run the following code in a Jupyter cell: ```python import frappe -frappe.init(site='mysite.localhost', sites_path='/workspace/development/frappe-bench/sites') +frappe.init(site='development.localhost', sites_path='/workspace/development/frappe-bench/sites') frappe.connect() frappe.local.lang = frappe.db.get_default('lang') frappe.db.connect() diff --git a/docs/setup-options.md b/docs/setup-options.md index 1d2dd72f..afe92ae7 100644 --- a/docs/setup-options.md +++ b/docs/setup-options.md @@ -17,7 +17,7 @@ Copy the example docker environment file to `.env`: cp example.env .env ``` -Note: To know more about environment variable [read here](./images-and-compose-files.md#configuration). Set the necessary variables in the `.env` file. +Note: To know more about environment variable [read here](./environment-variables.md). Set the necessary variables in the `.env` file. ## Generate docker-compose.yml for variety of setups diff --git a/docs/setup_for_linux_mac.md b/docs/setup_for_linux_mac.md index a6ca38bd..c805fe14 100644 --- a/docs/setup_for_linux_mac.md +++ b/docs/setup_for_linux_mac.md @@ -239,7 +239,7 @@ volumes: step3: run the docker ``` -docker-compose -f ./pwd.yaml up +docker-compose -f ./pwd.yml up ``` --- diff --git a/docs/troubleshoot.md b/docs/troubleshoot.md index eb5a9167..a43ed13c 100644 --- a/docs/troubleshoot.md +++ b/docs/troubleshoot.md @@ -36,13 +36,6 @@ EXIT; Note: For MariaDB 10.4 and above use `mysql.global_priv` instead of `mysql.user`. -### Letsencrypt companion not working - -- Nginx Letsencrypt Companion needs to be setup before starting ERPNext services. -- Are domain names in `SITES` variable correct? -- Is DNS record configured? `A Name` record needs to point to Public IP of server. -- Try Restarting containers. - ### docker-compose does not recognize variables from `.env` file If you are using old version of `docker-compose` the .env file needs to be located in directory from where the docker-compose command is executed. There may also be difference in official `docker-compose` and the one packaged by distro. Use `--env-file=.env` if available to explicitly specify the path to file. @@ -50,6 +43,5 @@ If you are using old version of `docker-compose` the .env file needs to be locat ### Windows Based Installation - Set environment variable `COMPOSE_CONVERT_WINDOWS_PATHS` e.g. `set COMPOSE_CONVERT_WINDOWS_PATHS=1` -- While using docker machine, port-forward the port 80 of VM to port 80 of host machine +- While using docker machine, port-forward the ports of VM to ports of host machine. (ports 8080/8000/9000) - Name all the sites ending with `.localhost`. and access it via browser locally. e.g. `http://site1.localhost` -- related issue comment https://github.com/frappe/frappe_docker/issues/448#issuecomment-851723912 diff --git a/example.env b/example.env index 68b88b54..fe979047 100644 --- a/example.env +++ b/example.env @@ -1,6 +1,6 @@ # Reference: https://github.com/frappe/frappe_docker/blob/main/docs/images-and-compose-files.md -ERPNEXT_VERSION=v14.29.0 +ERPNEXT_VERSION=v14.33.2 DB_PASSWORD=123 diff --git a/pwd.yml b/pwd.yml index 334165a4..f77e87a4 100644 --- a/pwd.yml +++ b/pwd.yml @@ -2,7 +2,7 @@ version: "3" services: backend: - image: frappe/erpnext:v14.29.0 + image: frappe/erpnext:v14.33.2 deploy: restart_policy: condition: on-failure @@ -11,7 +11,7 @@ services: - logs:/home/frappe/frappe-bench/logs configurator: - image: frappe/erpnext:v14.29.0 + image: frappe/erpnext:v14.33.2 deploy: restart_policy: condition: none @@ -39,7 +39,7 @@ services: - logs:/home/frappe/frappe-bench/logs create-site: - image: frappe/erpnext:v14.29.0 + image: frappe/erpnext:v14.33.2 deploy: restart_policy: condition: none @@ -90,7 +90,7 @@ services: - db-data:/var/lib/mysql frontend: - image: frappe/erpnext:v14.29.0 + image: frappe/erpnext:v14.33.2 deploy: restart_policy: condition: on-failure @@ -112,7 +112,7 @@ services: - "8080:8080" queue-default: - image: frappe/erpnext:v14.29.0 + image: frappe/erpnext:v14.33.2 deploy: restart_policy: condition: on-failure @@ -126,7 +126,7 @@ services: - logs:/home/frappe/frappe-bench/logs queue-long: - image: frappe/erpnext:v14.29.0 + image: frappe/erpnext:v14.33.2 deploy: restart_policy: condition: on-failure @@ -140,7 +140,7 @@ services: - logs:/home/frappe/frappe-bench/logs queue-short: - image: frappe/erpnext:v14.29.0 + image: frappe/erpnext:v14.33.2 deploy: restart_policy: condition: on-failure @@ -178,7 +178,7 @@ services: - redis-socketio-data:/data scheduler: - image: frappe/erpnext:v14.29.0 + image: frappe/erpnext:v14.33.2 deploy: restart_policy: condition: on-failure @@ -190,7 +190,7 @@ services: - logs:/home/frappe/frappe-bench/logs websocket: - image: frappe/erpnext:v14.29.0 + image: frappe/erpnext:v14.33.2 deploy: restart_policy: condition: on-failure diff --git a/tests/test_frappe_docker.py b/tests/test_frappe_docker.py index 1b1ca783..e8fcad0d 100644 --- a/tests/test_frappe_docker.py +++ b/tests/test_frappe_docker.py @@ -39,7 +39,7 @@ def assets_cb(text: str): @pytest.mark.parametrize( - ("url", "callback"), (("/", index_cb), ("/api/method/version", api_cb)) + ("url", "callback"), (("/", index_cb), ("/api/method/ping", api_cb)) ) def test_endpoints(url: str, callback: Any, frappe_site: str): check_url_content(