diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..3b6d5f3
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Amirreza Panahi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..60a5589
--- /dev/null
+++ b/app.py
@@ -0,0 +1,17 @@
+from flask import Flask, render_template
+
+app = Flask(__name__)
+
+
+@app.route("/")
+def home():
+ data = {
+ "en_name": "Mohammad Amin Mirzaee",
+ "fa_name": "محمد امین میرزائی",
+ "stuNumber": "04121129705027",
+ }
+ return render_template("index.html", data=data)
+
+
+if __name__ == "__main__":
+ app.run(debug=True)
diff --git a/examples/html-basic/app.py b/examples/html-basic/app.py
new file mode 100644
index 0000000..3a29631
--- /dev/null
+++ b/examples/html-basic/app.py
@@ -0,0 +1,48 @@
+
+
+from datetime import datetime
+
+from flask import Flask, render_template
+
+app = Flask(__name__)
+
+FEATURES = [
+ (
+ "render_template",
+ "EN: render_template is a function provided by Flask that renders an HTML template and returns it as a response to a client (browser). Flask uses a template engine called Jinja2. You write HTML files with placeholders or dynamic content, and render_template fills in those placeholders when the page is requested.",
+ ),
+ (
+ "render_template",
+ " فا: این تابع توسط فلسک ارائه شده و کد های اچ تی ام ال را که در پوشه تمپلیتس است رندر می کند، فلسک در واقع از انجین یا موتوری به نام جینجا2 استفاده می کند. شما می توانید فایل های اچ تی ام ال را به صورت مستقیم و یا پویا در فایل های پایتون به اچ تی ام ال وصل کنید و در خواست به صفحاتتون بفرستید",
+ ),
+ (
+ "/templates",
+ "EN: Store HTML files or templates that your Flask app will render. Flask automatically looks here when you call render_template.",
+ ),
+ (
+ "/templates",
+ "فا: ذخیره فایل های اچ تی ام ال و قالب ها در برنامه فلسک خود که رندر خواهد کرد. فلسک اتوماتیک این پوشه را رصد می کند در صورتیکه تابع رندر_تمپلیتس فراخوانی شود",
+ ),
+ (
+ "/static",
+ "EN: Store static files like CSS, JavaScript, images, fonts — anything that doesn’t change dynamically. Flask serves files from this folder automatically.",
+ ),
+ (
+ "/static",
+ "فا: تمامی فایل های استاتیک از جمله کد های سی اس اس و جاوا اسکریپت و عکس ها . فونت ها یا مدیا ها را ذخیره می کند که در حال تغییر به صورت پویا نیستند. فلسک این فایل ها را به صورت اتوماتیک برای شما تنظیم کرده و می توانید از مسیر آن استفاده کنید",
+ ),
+]
+
+
+@app.route("/")
+def homepage():
+ """Render a simple HTML page that uses dynamic data."""
+ return render_template(
+ "index.html",
+ features=FEATURES,
+ current_time=datetime.utcnow(),
+ )
+
+
+if __name__ == "__main__":
+ app.run(debug=True)
diff --git a/examples/html-basic/requirements.txt b/examples/html-basic/requirements.txt
new file mode 100644
index 0000000..95fef4e
--- /dev/null
+++ b/examples/html-basic/requirements.txt
@@ -0,0 +1 @@
+Flask==3.0.3
diff --git a/examples/html-basic/static/app.js b/examples/html-basic/static/app.js
new file mode 100644
index 0000000..b8e5576
--- /dev/null
+++ b/examples/html-basic/static/app.js
@@ -0,0 +1,19 @@
+document.addEventListener("DOMContentLoaded", () => {
+ const highlightButton = document.querySelector("[data-js='highlight']");
+ const timestampButton = document.querySelector("[data-js='timestamp']");
+ const container = document.querySelector("main");
+ const timestampTarget = document.querySelector("[data-js='timestamp-value']");
+
+ if (highlightButton) {
+ highlightButton.addEventListener("click", () => {
+ container.classList.toggle("highlighted");
+ });
+ }
+
+ if (timestampButton) {
+ timestampButton.addEventListener("click", () => {
+ const now = new Date().toISOString().replace("T", " ").slice(0, 19) + "Z";
+ timestampTarget.textContent = now;
+ });
+ }
+});
diff --git a/examples/html-basic/static/img/after.png b/examples/html-basic/static/img/after.png
new file mode 100644
index 0000000..e177a1c
Binary files /dev/null and b/examples/html-basic/static/img/after.png differ
diff --git a/examples/html-basic/static/img/before.png b/examples/html-basic/static/img/before.png
new file mode 100644
index 0000000..92745ff
Binary files /dev/null and b/examples/html-basic/static/img/before.png differ
diff --git a/examples/html-basic/static/img/full.png b/examples/html-basic/static/img/full.png
new file mode 100644
index 0000000..253e7cc
Binary files /dev/null and b/examples/html-basic/static/img/full.png differ
diff --git a/examples/html-basic/static/img/tree.png b/examples/html-basic/static/img/tree.png
new file mode 100644
index 0000000..aae5279
Binary files /dev/null and b/examples/html-basic/static/img/tree.png differ
diff --git a/examples/html-basic/static/style.css b/examples/html-basic/static/style.css
new file mode 100644
index 0000000..e9bbdcd
--- /dev/null
+++ b/examples/html-basic/static/style.css
@@ -0,0 +1,83 @@
+:root {
+ color-scheme: light dark;
+ font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ padding: 2.5rem 1rem 4rem;
+ background: #f3f4f6;
+ color: #111;
+}
+
+main {
+ max-width: 700px;
+ margin: 0 auto;
+ background: #fff;
+ border-radius: 12px;
+ padding: 2rem;
+ box-shadow: 0 12px 30px rgba(15, 23, 42, 0.15);
+ transition: transform 150ms ease, box-shadow 150ms ease;
+}
+
+main.highlighted {
+ transform: translateY(-2px);
+ box-shadow: 0 20px 40px rgba(37, 99, 235, 0.25);
+}
+
+h1 {
+ margin-top: 0;
+ font-size: 2.25rem;
+ color: #2563eb;
+}
+
+ul {
+ padding-left: 1.25rem;
+}
+
+li {
+ margin-bottom: 0.75rem;
+}
+
+footer {
+ margin-top: 2rem;
+ font-size: 0.85rem;
+ color: #475569;
+}
+
+code {
+ background: #f1f5f9;
+ color: #0f172a;
+ padding: 0.15rem 0.3rem;
+ border-radius: 4px;
+}
+
+.actions {
+ margin: 1.5rem 0;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+}
+
+button {
+ padding: 0.65rem 1.2rem;
+ border-radius: 999px;
+ border: none;
+ background: #2563eb;
+ color: #fff;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 120ms ease, transform 120ms ease;
+}
+
+button:hover {
+ background: #1d4ed8;
+}
+
+button:active {
+ transform: scale(0.98);
+}
diff --git a/examples/html-basic/templates/index.html b/examples/html-basic/templates/index.html
new file mode 100644
index 0000000..442c843
--- /dev/null
+++ b/examples/html-basic/templates/index.html
@@ -0,0 +1,88 @@
+
+
+
+
+ گزارش دوم – کار با HTML Templates در Flask
+
+
+
+
+
+
My Info
+
Mohammad Amin Mirzaee - 04121129705027
+
+
+ What is render_template & What is the role of the
+ /templates and /static folders?
+
+
+ {% for title, description in features %}
+
+ {{ title }}:
+ {{ description }}
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+ The current (UTC) time was rendered by Flask:
+
+ {{ current_time.strftime("%Y-%m-%d %H:%M:%S") }}Z
+
+
+
+
+
+
+
diff --git a/examples/postgresql/.env b/examples/postgresql/.env
new file mode 100644
index 0000000..ae87e12
--- /dev/null
+++ b/examples/postgresql/.env
@@ -0,0 +1,8 @@
+# PostgreSQL connection string in the form postgresql://USER:PASS@HOST:PORT/DBNAME
+DATABASE_URL=postgresql://me:root@localhost:5432/test
+
+# Port for the Flask development server
+PORT=5001
+
+# Enable the debugger and auto-reload (true/false)
+FLASK_DEBUG=true
diff --git a/examples/postgresql/.env.example b/examples/postgresql/.env.example
new file mode 100644
index 0000000..fc3d59b
--- /dev/null
+++ b/examples/postgresql/.env.example
@@ -0,0 +1,8 @@
+# PostgreSQL connection string in the form postgresql://USER:PASS@HOST:PORT/DBNAME
+DATABASE_URL=postgresql://demo_user:demo_pass@localhost:5432/demo
+
+# Port for the Flask development server
+PORT=5001
+
+# Enable the debugger and auto-reload (true/false)
+FLASK_DEBUG=true
diff --git a/examples/postgresql/app.py b/examples/postgresql/app.py
new file mode 100644
index 0000000..a3eae2f
--- /dev/null
+++ b/examples/postgresql/app.py
@@ -0,0 +1,491 @@
+import os
+from decimal import Decimal
+from typing import Any, Mapping, Sequence
+
+import psycopg2
+from psycopg2 import IntegrityError
+from psycopg2.extras import RealDictCursor
+from dotenv import load_dotenv
+from flask import Flask, Response, jsonify, request
+
+"""
+Flask + PostgreSQL learning lab.
+
+This single file now demonstrates every foundational database task you encounter
+in CRUD apps:
+
+- /init – create the table and helper indexes
+- /reset – drop and recreate the schema (danger zone, but educational)
+- /seed – insert sample rows with optional UPSERT behavior
+- /health – verify the database connection
+- /items – list items as JSON
+- /items/ – read/update/delete individual rows (JSON payloads)
+- /add – legacy query-param insert for quick URL experiments
+- /search – filter by name/value plus pagination
+- /stats – aggregate queries (min/max/avg)
+- /schema – inspect the table definition using information_schema
+- /list – tab-separated export for spreadsheets/CLI work
+
+Connection details stay in environment variables so you can point the app at any
+database without editing code.
+"""
+
+load_dotenv()
+
+DATABASE_URL = os.getenv(
+ "DATABASE_URL",
+ "postgresql://postgres:root@localhost:5432/postgres",
+)
+PORT = int(os.getenv("PORT", "5000"))
+DEBUG = os.getenv("FLASK_DEBUG", "false").lower() in {"1", "true", "yes"}
+TRUE_VALUES = {"1", "true", "yes", "on"}
+SELECT_COLUMNS = "id, name, value, note, created_at, updated_at"
+
+SEED_ITEMS = [
+ {"name": "Desk", "value": 199.99, "note": "Spacious work surface"},
+ {"name": "Chair", "value": 89.5, "note": "Ergonomic and adjustable"},
+ {"name": "Lamp", "value": 35.0, "note": "LED task lighting"},
+ {"name": "Notebook", "value": 6.25, "note": "Grid-paper notebook"},
+ {"name": "Plant", "value": 18.75, "note": "Adds a splash of green"},
+]
+
+app = Flask(__name__)
+
+
+def get_conn() -> psycopg2.extensions.connection:
+ """Return a new psycopg2 connection using DATABASE_URL."""
+ return psycopg2.connect(DATABASE_URL)
+
+
+def create_items_table(drop_existing: bool = False) -> None:
+ """(Re)create the demo table and the indexes we rely on."""
+ with get_conn() as conn, conn.cursor() as cur:
+ if drop_existing:
+ cur.execute("DROP TABLE IF EXISTS items")
+ cur.execute(
+ """
+ CREATE TABLE IF NOT EXISTS items (
+ id SERIAL PRIMARY KEY,
+ name TEXT NOT NULL UNIQUE,
+ value DOUBLE PRECISION NOT NULL CHECK (value >= 0),
+ note TEXT NOT NULL DEFAULT '',
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+ )
+ """
+ )
+ cur.execute("CREATE INDEX IF NOT EXISTS idx_items_value ON items (value)")
+ cur.execute(
+ "CREATE INDEX IF NOT EXISTS idx_items_created_at ON items (created_at)"
+ )
+
+
+def _floatify(value: Any) -> float | None:
+ if value is None:
+ return None
+ if isinstance(value, Decimal):
+ return float(value)
+ return float(value)
+
+
+def _serialize_item(row: Mapping[str, Any]) -> dict[str, Any]:
+ """Convert psycopg2 rows into JSON-friendly data."""
+ return {
+ "id": row["id"],
+ "name": row["name"],
+ "value": _floatify(row.get("value")),
+ "note": row.get("note") or "",
+ "created_at": row.get("created_at").isoformat()
+ if row.get("created_at")
+ else None,
+ "updated_at": row.get("updated_at").isoformat()
+ if row.get("updated_at")
+ else None,
+ }
+
+
+def _run_select(sql: str, params: Sequence[Any] | None = None) -> list[dict[str, Any]]:
+ with get_conn() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
+ cur.execute(sql, params or ())
+ rows = cur.fetchall()
+ return [_serialize_item(row) for row in rows]
+
+
+def _select_one(sql: str, params: Sequence[Any] | None = None) -> dict[str, Any] | None:
+ with get_conn() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
+ cur.execute(sql, params or ())
+ row = cur.fetchone()
+ return _serialize_item(row) if row else None
+
+
+def _coerce_name(raw: Any) -> str:
+ name = str(raw or "").strip()
+ if not name:
+ raise ValueError("name is required")
+ return name
+
+
+def _coerce_value(raw: Any) -> float:
+ try:
+ value = float(raw)
+ except (TypeError, ValueError):
+ raise ValueError("value must be a number")
+ if value < 0:
+ raise ValueError("value must be >= 0")
+ return value
+
+
+def _coerce_note(raw: Any) -> str:
+ return str(raw or "").strip()
+
+
+def _parse_create_payload(data: Mapping[str, Any]) -> tuple[str, float, str]:
+ return (
+ _coerce_name(data.get("name")),
+ _coerce_value(data.get("value")),
+ _coerce_note(data.get("note")),
+ )
+
+
+def _parse_partial_payload(data: Mapping[str, Any]) -> dict[str, Any]:
+ fields: dict[str, Any] = {}
+ if "name" in data:
+ fields["name"] = _coerce_name(data.get("name"))
+ if "value" in data:
+ fields["value"] = _coerce_value(data.get("value"))
+ if "note" in data:
+ fields["note"] = _coerce_note(data.get("note"))
+ if not fields:
+ raise ValueError("provide at least one of name, value, or note")
+ return fields
+
+
+def _insert_item(name: str, value: float, note: str) -> dict[str, Any]:
+ with get_conn() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
+ cur.execute(
+ f"""
+ INSERT INTO items (name, value, note)
+ VALUES (%s, %s, %s)
+ RETURNING {SELECT_COLUMNS}
+ """,
+ (name, value, note),
+ )
+ row = cur.fetchone()
+ return _serialize_item(row)
+
+
+def _update_item(item_id: int, fields: Mapping[str, Any]) -> dict[str, Any] | None:
+ if not fields:
+ return None
+ assignments = ", ".join(f"{column} = %s" for column in fields)
+ params = [*fields.values(), item_id]
+ with get_conn() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
+ cur.execute(
+ f"""
+ UPDATE items
+ SET {assignments}, updated_at = NOW()
+ WHERE id = %s
+ RETURNING {SELECT_COLUMNS}
+ """,
+ params,
+ )
+ row = cur.fetchone()
+ return _serialize_item(row) if row else None
+
+
+def _delete_item(item_id: int) -> bool:
+ with get_conn() as conn, conn.cursor() as cur:
+ cur.execute("DELETE FROM items WHERE id = %s", (item_id,))
+ return cur.rowcount > 0
+
+
+def _as_bool(value: Any) -> bool:
+ if isinstance(value, bool):
+ return value
+ return str(value or "").strip().lower() in TRUE_VALUES
+
+
+def _parse_limit(raw: Any, default: int = 20) -> int:
+ try:
+ limit = int(raw) if raw is not None else default
+ except (TypeError, ValueError):
+ limit = default
+ return max(1, min(100, limit))
+
+
+@app.get("/health")
+def health_check():
+ """Simple connection test that also returns the database server time."""
+ with get_conn() as conn, conn.cursor() as cur:
+ cur.execute("SELECT NOW()")
+ (current_time,) = cur.fetchone()
+ return jsonify({"status": "ok", "database_time": current_time.isoformat()})
+
+
+@app.get("/init")
+def init_db():
+ """Create the items table if it does not yet exist."""
+ create_items_table(drop_existing=False)
+ return "ok: table ready\n"
+
+
+@app.post("/reset")
+def reset_db():
+ """Drop the table and recreate it from scratch."""
+ create_items_table(drop_existing=True)
+ return jsonify({"status": "ok", "message": "table dropped and recreated"})
+
+
+@app.post("/seed")
+def seed_data():
+ """
+ Insert a batch of sample rows.
+
+ Accepts JSON:
+ {
+ "replace": true, # optional, truncate before inserting
+ "items": [
+ {"name": "...", "value": 1.23, "note": "..."},
+ ...
+ ]
+ }
+ """
+
+ payload = request.get_json(silent=True) or {}
+ replace = _as_bool(payload.get("replace") or request.args.get("replace"))
+ dataset = payload.get("items") or SEED_ITEMS
+ if not isinstance(dataset, list):
+ return jsonify({"error": "items must be a JSON list"}), 400
+
+ parsed_rows: list[tuple[str, float, str]] = []
+ for entry in dataset:
+ if not isinstance(entry, Mapping):
+ return jsonify({"error": "each item must be an object"}), 400
+ try:
+ parsed_rows.append(_parse_create_payload(entry))
+ except ValueError as exc:
+ return jsonify({"error": f"invalid item: {exc}"}), 400
+
+ if not parsed_rows:
+ return jsonify({"error": "no items to insert"}), 400
+
+ create_items_table(drop_existing=False)
+ with get_conn() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
+ if replace:
+ cur.execute("TRUNCATE TABLE items RESTART IDENTITY")
+ cur.executemany(
+ """
+ INSERT INTO items (name, value, note)
+ VALUES (%s, %s, %s)
+ ON CONFLICT (name) DO UPDATE
+ SET value = EXCLUDED.value,
+ note = EXCLUDED.note,
+ updated_at = NOW()
+ """,
+ parsed_rows,
+ )
+ cur.execute("SELECT COUNT(*) AS total FROM items")
+ total = cur.fetchone()["total"]
+
+ return jsonify(
+ {
+ "status": "ok",
+ "processed": len(parsed_rows),
+ "replace": replace,
+ "total_rows": total,
+ }
+ )
+
+
+@app.get("/items")
+def list_items_json():
+ """Return all rows as JSON."""
+ rows = _run_select(f"SELECT {SELECT_COLUMNS} FROM items ORDER BY created_at")
+ return jsonify({"count": len(rows), "items": rows})
+
+
+@app.post("/items")
+def create_item():
+ """Create a row from a JSON payload."""
+ data = request.get_json(silent=True) or {}
+ try:
+ name, value, note = _parse_create_payload(data)
+ except ValueError as exc:
+ return jsonify({"error": str(exc)}), 400
+ try:
+ item = _insert_item(name, value, note)
+ except IntegrityError:
+ return jsonify({"error": "name must be unique"}), 409
+ return jsonify(item), 201
+
+
+@app.get("/items/")
+def get_item(item_id: int):
+ """Fetch a single row by ID."""
+ row = _select_one(
+ f"SELECT {SELECT_COLUMNS} FROM items WHERE id = %s", (item_id,)
+ )
+ if not row:
+ return jsonify({"error": "item not found"}), 404
+ return jsonify(row)
+
+
+@app.put("/items/")
+def replace_item(item_id: int):
+ """Replace an existing row (requires name + value)."""
+ data = request.get_json(silent=True) or {}
+ try:
+ name, value, note = _parse_create_payload(data)
+ except ValueError as exc:
+ return jsonify({"error": str(exc)}), 400
+ try:
+ row = _update_item(item_id, {"name": name, "value": value, "note": note})
+ except IntegrityError:
+ return jsonify({"error": "name must be unique"}), 409
+ if not row:
+ return jsonify({"error": "item not found"}), 404
+ return jsonify(row)
+
+
+@app.patch("/items/")
+def update_item(item_id: int):
+ """Partially update a row."""
+ data = request.get_json(silent=True) or {}
+ try:
+ fields = _parse_partial_payload(data)
+ except ValueError as exc:
+ return jsonify({"error": str(exc)}), 400
+ try:
+ row = _update_item(item_id, fields)
+ except IntegrityError:
+ return jsonify({"error": "name must be unique"}), 409
+ if not row:
+ return jsonify({"error": "item not found"}), 404
+ return jsonify(row)
+
+
+@app.delete("/items/")
+def delete_item(item_id: int):
+ """Remove a row permanently."""
+ if not _delete_item(item_id):
+ return jsonify({"error": "item not found"}), 404
+ return jsonify({"status": "ok", "deleted_id": item_id})
+
+
+@app.get("/search")
+def search_items():
+ """Filter by partial name and/or numeric ranges."""
+ name_query = (request.args.get("q") or "").strip()
+ min_value_raw = request.args.get("min_value")
+ max_value_raw = request.args.get("max_value")
+ limit = _parse_limit(request.args.get("limit"))
+
+ try:
+ min_value = _coerce_value(min_value_raw) if min_value_raw else None
+ max_value = _coerce_value(max_value_raw) if max_value_raw else None
+ except ValueError as exc:
+ return jsonify({"error": str(exc)}), 400
+
+ conditions = []
+ params: list[Any] = []
+ if name_query:
+ conditions.append("name ILIKE %s")
+ params.append(f"%{name_query}%")
+ if min_value is not None:
+ conditions.append("value >= %s")
+ params.append(min_value)
+ if max_value is not None:
+ conditions.append("value <= %s")
+ params.append(max_value)
+
+ where_clause = f"WHERE {' AND '.join(conditions)}" if conditions else ""
+ params.append(limit)
+
+ sql = f"""
+ SELECT {SELECT_COLUMNS}
+ FROM items
+ {where_clause}
+ ORDER BY value DESC
+ LIMIT %s
+ """
+ rows = _run_select(sql, params)
+ return jsonify({"returned": len(rows), "items": rows})
+
+
+@app.get("/stats")
+def stats():
+ """Aggregate information about the table."""
+ with get_conn() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
+ cur.execute(
+ """
+ SELECT
+ COUNT(*) AS total_rows,
+ COALESCE(SUM(value), 0) AS total_value,
+ COALESCE(MIN(value), 0) AS min_value,
+ COALESCE(MAX(value), 0) AS max_value,
+ COALESCE(AVG(value), 0) AS avg_value
+ FROM items
+ """
+ )
+ row = cur.fetchone()
+
+ return jsonify(
+ {
+ "total_rows": row["total_rows"],
+ "total_value": _floatify(row["total_value"]),
+ "min_value": _floatify(row["min_value"]),
+ "max_value": _floatify(row["max_value"]),
+ "avg_value": _floatify(row["avg_value"]),
+ }
+ )
+
+
+@app.get("/schema")
+def describe_schema():
+ """Inspect the table definition via information_schema."""
+ with get_conn() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
+ cur.execute(
+ """
+ SELECT column_name, data_type, is_nullable, column_default
+ FROM information_schema.columns
+ WHERE table_name = 'items'
+ ORDER BY ordinal_position
+ """
+ )
+ columns = cur.fetchall()
+ return jsonify({"columns": columns})
+
+
+@app.get("/add")
+def add_item():
+ """Insert a new row using query parameters (e.g. ?name=foo&value=42)."""
+ data = {
+ "name": request.args.get("name"),
+ "value": request.args.get("value"),
+ "note": request.args.get("note", ""),
+ }
+ try:
+ name, value, note = _parse_create_payload(data)
+ except ValueError as exc:
+ return f"error: {exc}\n", 400
+ try:
+ item = _insert_item(name, value, note)
+ except IntegrityError:
+ return "error: name must be unique\n", 409
+ return f"ok: inserted id={item['id']}\n"
+
+
+@app.get("/list")
+def list_items_text():
+ """Return the stored items in a tab-separated plain-text response."""
+ with get_conn() as conn, conn.cursor() as cur:
+ cur.execute("SELECT id, name, value, note FROM items ORDER BY id")
+ rows = cur.fetchall()
+ lines = ["id\tname\tvalue\tnote"] + [
+ f"{row[0]}\t{row[1]}\t{row[2]:.2f}\t{row[3]}" for row in rows
+ ]
+ return Response("\n".join(lines) + "\n", mimetype="text/plain")
+
+
+if __name__ == "__main__":
+ app.run(host="127.0.0.1", port=PORT, debug=DEBUG)
diff --git a/examples/postgresql/requirements.txt b/examples/postgresql/requirements.txt
new file mode 100644
index 0000000..f95fdff
--- /dev/null
+++ b/examples/postgresql/requirements.txt
@@ -0,0 +1,3 @@
+Flask
+psycopg2-binary
+python-dotenv
diff --git a/examples/postgresql/static/img/curl.png b/examples/postgresql/static/img/curl.png
new file mode 100644
index 0000000..f80c6f5
Binary files /dev/null and b/examples/postgresql/static/img/curl.png differ
diff --git a/examples/postgresql/static/img/postgerSQL.png b/examples/postgresql/static/img/postgerSQL.png
new file mode 100644
index 0000000..a964d28
Binary files /dev/null and b/examples/postgresql/static/img/postgerSQL.png differ
diff --git a/examples/postgresql/static/img/successful_execution.png b/examples/postgresql/static/img/successful_execution.png
new file mode 100644
index 0000000..f01ab17
Binary files /dev/null and b/examples/postgresql/static/img/successful_execution.png differ
diff --git a/examples/postgresql/static/img/table.png b/examples/postgresql/static/img/table.png
new file mode 100644
index 0000000..b10d6c1
Binary files /dev/null and b/examples/postgresql/static/img/table.png differ
diff --git a/get-pip.py b/get-pip.py
new file mode 100644
index 0000000..22f29f8
--- /dev/null
+++ b/get-pip.py
@@ -0,0 +1,27368 @@
+#!/usr/bin/env python
+#
+# Hi There!
+#
+# You may be wondering what this giant blob of binary data here is, you might
+# even be worried that we're up to something nefarious (good for you for being
+# paranoid!). This is a base85 encoding of a zip file, this zip file contains
+# an entire copy of pip (version 25.3).
+#
+# Pip is a thing that installs packages, pip itself is a package that someone
+# might want to install, especially if they're looking to run this get-pip.py
+# script. Pip has a lot of code to deal with the security of installing
+# packages, various edge cases on various platforms, and other such sort of
+# "tribal knowledge" that has been encoded in its code base. Because of this
+# we basically include an entire copy of pip inside this blob. We do this
+# because the alternatives are attempt to implement a "minipip" that probably
+# doesn't do things correctly and has weird edge cases, or compress pip itself
+# down into a single file.
+#
+# If you're wondering how this is created, it is generated using
+# `scripts/generate.py` in https://github.com/pypa/get-pip.
+
+import sys
+
+this_python = sys.version_info[:2]
+min_version = (3, 9)
+if this_python < min_version:
+ message_parts = [
+ "This script does not work on Python {}.{}.".format(*this_python),
+ "The minimum supported Python version is {}.{}.".format(*min_version),
+ "Please use https://bootstrap.pypa.io/pip/{}.{}/get-pip.py instead.".format(*this_python),
+ ]
+ print("ERROR: " + " ".join(message_parts))
+ sys.exit(1)
+
+
+import os.path
+import pkgutil
+import shutil
+import tempfile
+import argparse
+import importlib
+from base64 import b85decode
+
+
+def include_setuptools(args):
+ """
+ Install setuptools only if absent, not excluded and when using Python <3.12.
+ """
+ cli = not args.no_setuptools
+ env = not os.environ.get("PIP_NO_SETUPTOOLS")
+ absent = not importlib.util.find_spec("setuptools")
+ python_lt_3_12 = this_python < (3, 12)
+ return cli and env and absent and python_lt_3_12
+
+
+def include_wheel(args):
+ """
+ Install wheel only if absent, not excluded and when using Python <3.12.
+ """
+ cli = not args.no_wheel
+ env = not os.environ.get("PIP_NO_WHEEL")
+ absent = not importlib.util.find_spec("wheel")
+ python_lt_3_12 = this_python < (3, 12)
+ return cli and env and absent and python_lt_3_12
+
+
+def determine_pip_install_arguments():
+ pre_parser = argparse.ArgumentParser()
+ pre_parser.add_argument("--no-setuptools", action="store_true")
+ pre_parser.add_argument("--no-wheel", action="store_true")
+ pre, args = pre_parser.parse_known_args()
+
+ args.append("pip")
+
+ if include_setuptools(pre):
+ args.append("setuptools")
+
+ if include_wheel(pre):
+ args.append("wheel")
+
+ return ["install", "--upgrade", "--force-reinstall"] + args
+
+
+def monkeypatch_for_cert(tmpdir):
+ """Patches `pip install` to provide default certificate with the lowest priority.
+
+ This ensures that the bundled certificates are used unless the user specifies a
+ custom cert via any of pip's option passing mechanisms (config, env-var, CLI).
+
+ A monkeypatch is the easiest way to achieve this, without messing too much with
+ the rest of pip's internals.
+ """
+ from pip._internal.commands.install import InstallCommand
+
+ # We want to be using the internal certificates.
+ cert_path = os.path.join(tmpdir, "cacert.pem")
+ with open(cert_path, "wb") as cert:
+ cert.write(pkgutil.get_data("pip._vendor.certifi", "cacert.pem"))
+
+ install_parse_args = InstallCommand.parse_args
+
+ def cert_parse_args(self, args):
+ if not self.parser.get_default_values().cert:
+ # There are no user provided cert -- force use of bundled cert
+ self.parser.defaults["cert"] = cert_path # calculated above
+ return install_parse_args(self, args)
+
+ InstallCommand.parse_args = cert_parse_args
+
+
+def bootstrap(tmpdir):
+ monkeypatch_for_cert(tmpdir)
+
+ # Execute the included pip and use it to install the latest pip and
+ # any user-requested packages from PyPI.
+ from pip._internal.cli.main import main as pip_entry_point
+ args = determine_pip_install_arguments()
+ sys.exit(pip_entry_point(args))
+
+
+def main():
+ tmpdir = None
+ try:
+ # Create a temporary working directory
+ tmpdir = tempfile.mkdtemp()
+
+ # Unpack the zipfile into the temporary directory
+ pip_zip = os.path.join(tmpdir, "pip.zip")
+ with open(pip_zip, "wb") as fp:
+ fp.write(b85decode(DATA.replace(b"\n", b"")))
+
+ # Add the zipfile to sys.path so that we can import it
+ sys.path.insert(0, pip_zip)
+
+ # Run the bootstrap
+ bootstrap(tmpdir=tmpdir)
+ finally:
+ # Clean up our temporary working directory
+ if tmpdir:
+ shutil.rmtree(tmpdir, ignore_errors=True)
+
+
+DATA = b"""
+P)h>@6aWAK2ms3fSz9=Nlw0@!003bD000jF003}la4%n9X>MtBUtcb8c|DL%OT<77#qaYeLNB_YQ}7R
+JLBWgQMLc*D8D`sbJJ4o^By}nH;Y}-R7|8JQc>H)h=LtgSXPp^CfHalN3Xv#l)Rak_3*j4C>~Hr+sIG
+4Pb>*Dvu!kuoI*)vi2F4`%Dav2)18n<}T4!jWSs$bTq|)*=0iTP-{H3s6e~1QY-O00;of09jja_pjHy0RRA2
+0{{RI0001RX>c!JUu|J&ZeL$6aCu!)OK;mS48HqU5b43r;JP^vOMxACEp{6QLy+m1h%E`C9MAjpBNe-
+8r;{H19{ebpf{zJ27j)n8%0=-6Z#elILRo@w9oRWWbO{z8ujDS!QAC@3T%nJCf;1rX6ghzu#Z}R@K&*?Hgj1WFD91+adaM4G`4Xs@*hA^t@nbDYdL)-aOjsW~3}QVVby(8=@7U$
+Fzj5Y{w!2hUUH`?e9j7WDA;>-1aos>7j{2$~BfyL8p@__Y98dsP#Bs7^lWF
+=e_gr;(4^?am?Cp93+7b-!?~nb}-$cPSR1zckA*zNp!)$;YjlZrfn&RWNM}=QA7*cb8A{(9@{5!vBfq
+rEMoeu5FvJZngI@N#4#(2v$WnMGCAVD?b9t8W^qDfcFBe5ZZF%dPAPaq#>ikclG~yPvCg`JUGb_W2#PdCXxx}7!|T*xc9qdnTILbO-nAJaF2
+~0snMFDU<%E01X4*yW9@|}F2;vY~;0|XQR000O8%K%whB^=tD)dBzjss#W56#xJLaA|NaUte%(a4
+m9mZf<3AUtcb8d3{vDPTN2bz56Q$bEqvD7eOzLnyL~CDi@L_;ZRYu+Sp^V)ZR6_YZ?pj@13#Z1R`1=l
+J)M)n>TOXIt;_f2D8Q^;6`S?Y{9RUgUr+|m;!25C-6tno(2iIDhjlyJ)nM4*651XX%H+qrBEdT{cBla
+4$^`0^qPP-6zv*|ge-jzUzxn2=uGMl9#)iA)y8^Cdr~rxdixH}OOJhxFbsp>7(O2Tg09*VTBnRAqE#)
+uTB%a`7P2*FzrkVV`K)SOhdyilnqJR#!6l}Q6a+(^)-m{nsTFZ3tf`=GYik||DD|c)gW1pJ_vy8mPk!
+87%_j>OLv)_N=Qs$09E*XCaNb7Sbvyz%2H(~=0(GyA#Q^BB=o_mcOvCiSC>?bfF%-ta6OhP5HUX=GiK
+PR!(uKJlo!!9~IAAmCk)?77i`J23la2CGx64oXMzMaXkQ<~~8EU?%I}z$$rRNujEIu~M()ri%^Gi%ri
+C!gNA@cLO=l6KV$(xV^&hZYbU&TCtOIChO4g;gfcAY_>ak~kgLxGa?L$cMXVJ{&`q`lnqv$j}Cr3vW0
+iSMRu8%^e>;b`+HM=<$xdKPpu@6SuMN-LR>$cFaIP$0f`iClb~O`=>COC
+NvJms>bV(-KMn=QG5PXY-h9vs;@fOIZ_lmTi^Fg`mulO!YZVO^KOIvSWtj)HD-~+vPht4%90
+Btz&yv-M$^(udyl?*`G;BgAr}tWa5RRHyc3Sz7-4H^#tC)@T$~!*>z3|0?b+NOYH~Nw+WUjLL%ySwnL
+u=yutnX)PsolsJX_Jx&=d9p7_`6i5S
+Mvz$qHBvD4Gc2vqMK2J#u@ySRoi8HJ74pBUZpQaDYr)B{xbde@6aWAK2ms3fSz8Tl@oYE&00931000>P003}la4%nJZggdGZ
+eeUMUtei%X>?y-E^v8uQNc>YKn%UlSA_Ml1)W|5bwQ7w#FICXGTWwYU^+7-sY}6+H?6MX!CdkPkC&I1
+p7R7v)6Y6HHVx2JGAo3XvIeD`#JPUu6e_-52zR!@h!P7KI~18)C%HP@fr
+1dD$kQ8NRuGKz%ZZysu2=G*Ual7)rq;5cIqz_b_FXoN_lu6z|qu{_j%fT!+RBl=guKIY1=QS5bb04|v
+WA;eKlsTs@d!Jvgx1?RGCC*+Bw@QIOxwu-SziJ7_J091)~tDk`9(d78okVj;x!LdG5$Q)?DBIa2R7@M
+sdD>u3!!MCee1<#q{z2%~C|LtPJ~<9zgOp6arcP+QP7iOeYV&Gp@_KO5Zof3NVEl$Vli`umm>uNm@}6
+-N7T`WbHNRPGZ{O9KQH000080LuVbTZC7+>nRWb0C_h602%-Q0B~t=FJEbHbY*gGVQepAb!lv5UuAA~
+E^v9>8(nkUw((uR0-+yFrX-X2q0_m^wd0Sot*I+%BspzHqhW9)PGZd?Spt-FQT5-uiw_b2d1s}4N^i!
+#Be+;B_Inpl5Cm`fvMR``zAL+?-m+Sdp0k2%nvRsbsi-KMniPFk);EL~B^P9kGvF}@f}^8N*KA3aZF<
+pnEXzo_ZJSOITGx$`bNSJc9;=$08<=Ju8*YBJRNPkO+C1`7u;KS^fD-IM+;_B9OXf{gv0N@-);#SB*0
+JJUnTrWbO4qr8I~J^?>xwBLv1{3Y;Zw_TGnJ}@t*Rh5my`=<)FZL^~625o}pcd%eCnr;^pd<}
+22FJ)bz*=$^OTO1Mi%peDF_MmsfKy%=6SmI2LzL$gh5Wv3i9}I8-dl?KxJ)T=!kr?ud!sb^GqNCbxgo
+FCSF2L}s<$GFj7AcbP!w}kNz=9M2dc{Q-6Zr4?=;#RL2UIVOmq0cx(1t+%uVZ;W$zvt^(sYKMB~${b-kW
+x-&tzv2jRGzH^>eNbWH%qa&}-UAD1jVzMy!IIE_RJ(M5)H)8L3KfOXk*)g;PdB_^c~da`o_t4w3}q}u
+!n!O=+g&i2n^edh2k(?&Xx4s)YlUFaj;|f9P|z}v69cYj_{@6{+fMBo-=m$|SYrvc?giNZAckTBy7Y?
+`lEUaaD_VuoFUZ=yrgyZgWLQT8As|kHt-w-{zJNtR4M^TGj3Q*cvqpb>^}S}$xnvLr*>+o6mA?WcY>z
+Y>pzlfc_cV5x+~Ks3dP6*Q%$OBoOmrOe-uwRv1qb?qHR73dDXhX-l$ClBLUT`s_xY9I7AQej%Cbo?)z8!yJ@}VN@Fa*2
+8)Mzo25I~&8(5GO>NJ!E&9r$@M`0l-&_4%pNkQw%Y<>s2XGF?UkA7jG|+lHvUhi@udTDW=A_*#;X<&Y
+^u`cg{XR!{?(90NBCS~;>{ZcerVuY=VQlfe|YyKb7wBmh3^+~}>7xuvGleVI+D1HkDR=;YP)S7!P&&L
+HwZEFN1qyFCH&iu{XAmu9wSgpHrK2q=xm&i?Odhxq}>=J+godwOzldH(wNKwu;!=&kIY+^mzy1#E~7HFB$=oWFm8rU+}WGY$DiI=`%B4X19=)p!K=`!ReK2BGl*
+zCM-Ap9Ndn1VqV;L{Ji1;YU7s&j+56S&%g^VlW#G>XULNA$4cIPt(q-`KkLjzxeFv#Q&z7zajDDwq%X
+5{?LOZ0nIOd;w96{(xPP~7&$)
+u2qN<}jf~C#nEVO!+j$tc@VST0{5363I*t^aXj%$1=|L@aThCiv?>(tC*DHC0Y}tahtw^MAgFrM1zWN
+TdYwTEVz>?Y)qK6cUg&0?bGqCE?qq&+otGYzI0M+CX3VWG|~dS(6-@1rN8M6V>IMsgO{*7#Z&EkO00S
+B8&H%Q5*@mRXaR)F4EXrB$|@P9t4s%al+FPEslWv!NZ#BkQD|QlAQ#e-4B!FApK`k)
+=CU!fESO!qDGe%JVi5;U9v4#CYy;_-&K6bI@58?R*P|TMnswiS3y$321d}U@2{^TpYOUmD4|ej
+7WfZ~>&1k4Z-qooPvoowX2F6M!fRXU?9zI`G^i8WtDNJh6`14~q{}D-d}TOxc;gAeB@zxO!tZOF&l`g
+dikFAoVN4PPkk~V>T^euE3yn02t_P=BWH|g4GBs#|ps)shT}Lx?<^a}gvz&F}FhYbFiM6K9qpeDYG4<
+!<<1(@KEe$9)8dkY
+_4Hw#8%I9b~vkbBhz9ZvFFL=rcXFOhh%h6?kS0V!~)*<@#(*)Gm+u4S^Yc_A8#=4f~uIpzq*+zL9qKb
+re^ykZI#!C7J+s6JyTkvQWKEzO<&RO03u!Jxg9e*Cv@N)79!IEXCzp_NQTwW@rf?fjz657p0BKW?E-*
+;AA+Z+Q81o`TMoh&xLwQG23EiTRG0SoB2eb01Dimq?xU_*Qatl|3Z(y0BT|y~TXkqQqB%`maM#nM0&?9gPzhxd&zGB^^jCbGhCGQx8FKabpfKit=
+yr{_*R|-aLH338GF-CSJtjLL}O~$u_MXnn~dsc(9~SrVt+4rU&%Sl1qB&?47IcBEhohMphBo6
+a^b9lXBnoe3)>Qt=)G3;1>?5HB^pm4+O+#+Z(&E=H^>~+nZ&JC2h=PmmW|9t
+u2TdkUhv?4&bX;acnVGke&9=a$!_5r+%R39f5PBBmJ)T|;B}{E(2Gs-C(
+GTU``ol%71^fpaBk`{&HbgcVCV(+6obLEcQ@RZiitXB#7j{Vs2Ait=#Yp>(KW3WW1_?eX|Hr@xO=ZhA
+^FmkP{7j)<(%nT3)qvwpI3|P3A{WMlz{RilZkMe2HB@+adF8s)|Oj;E9jlxyxVy(5beoq(F
+YuHrHE;D~BI1B2X6e@mHpj8uwMKfHQ;+*62)$GdhrM>)Q>g+VIr9Fq9Xkz1er)1Iu!*mN~(`eEY0%jE
+Of70WV(>ab-8MnDtqp&0ayOF0Go*194fm12YS!kqtM4_
+$eOZx%qe?{)njRmuP7?o2nJXY38#oi7zr+1j^-(jZfpu#Y_@DGPn^hP{~;_}(+%a0h9&wjrAczSYj`u
+SpYesO%c8pkBWh{8&gjsI{$r0qo-s6QMavx6(OuG4umz^;IGq>wSxK%~5_N?{Tb&f3U|P_1n18k2Kf|
+MduFF{^J2>O1krsV+#07i3&j1sAC~)1-CEP+4c}1>A>y*p0WwSRT51^l<0^oIT_j-ho}no3K}Jz-g==
+a@Jx4|JpevG0>0}L1Jse&4P*b!o5%XwIdf)!aLe29Ywjy{A(=UjiRI=xh?k(C$+ZdIo!iNBjUDc!(q9
+pNlbf%?))n~7t_mO;gRj~RKqTFxF-NS`0@|DM$YKVi%eX3*BGzk;mVcO7zL-KE-#T+_}Z%K6M05ZmFm
+FUJ#5pLo5JCYC?@w5oK6u*;uI0wLWQ@0_M*WlHbKL0Gt18vY{L9^rNZY5K)JbTT=(u4hlf0@Mm}8JlA
+;I(;9v?ZtL#1ZnnT}x+fS&^c&ip;c?eUW#f*7-}9B<~SM4S
+b0G~bwY^LOUxYW+`bj*BDJqj=+2M8Kzs)UJGPK^WXtkfT9dm=Tu2!0f@k)y6mUdt8MzT6-
+lXqrJS89Bn>V2?KcWlr(22as^Y{z4gvhLaBrThzpC2lB{GAr>W!@5T=6`ID$tVSR;2tB`^)?rK!_6z4
+AVR&G|rKN!&LR`uGH9&0s4q(q-2lLE~HZ92}cpREU%jKhu?rEBz%=@zE;r=Re%{!c84l=tG*+2ogw^6
+GHuX+%BrE5Hc6Rw7w>2EfpInPwp-p^5Yi5XJ2H2-Xre6q$Q%ub?JyFy-H_b?83+EJ)}ZSOF~k(RztWb<=ufSF%TSJm8yk
+0pPW_+M=Jvee&W`|qSVZT${h=nssSIbQ?02^7y*K_}8<57&mIGxuli?*Y!S9tipk+5H6kcap@pbmw5}
+HU7!Lb2j<^W^5fTA#*rs#PJsLD4fWORo68gWDWlTP)h>@6aWAK2ms3fSzE;-+kFQN003zy000&M003}
+la4%nJZggdGZeeUMV_{=xWiD`e-CAvr+r|<8e!pU?zz9{yJlAmCR)C5+&N+7Bm%w)54~GM>5?9jtC@#
+Y#PgdIg_dYYbB$py}J~vHTR4~rszRb@1JhRkfGI=UjTP0q}TyE4(<<(>|IXhdIW+#$lT~~Ffl0@iTVa
+iI#JU5lBw8`z+nIGFqyUp~ndHiTi-h9u@W~1{>^JuK2TgZxbG(>;EqnoG>1(rACPx6Cjq|im2+^9S?W
+n9SBwIr%>B{#NN`(AElLg$q#i&EillFOaykKCxzg7MoZ)|Jj$k}H{;T(4xNe^yK`WQGanGKv
+&Au1;4fdoTwn}Bsbf$Rg$j+TfRc7NcjV)%;<4Wy
+{1RS+$#j|6_l!uw1Y0M_qI#2CsDv+hs2H85P49RzPM*g5mv1lA4-l*y&k3|WqI7y~wXK&uV`2NM~eWI7Wnw-hkau
+C(TrQ>ItC<9(>a?)1pUDqq1!(S+Aerqg(y(~NuEH8B}9`exG2xx7e#4qbgC?T
+mSQ>e7SkA6n^L5*l7g*8ZiC1rQgh;e=XQ|E=i)uKmc~@1l?vZ^DsIkoyIAqCx2}>TvMO88LJDAuHUHY
+=o?%vIUBJN8xZO8xr@+2~lOU;dWCS=iHYUf3wS}xvJJoHZqvLMNqQ9Na5BPax
+zUV|L^h&M92NVrtF7E5&k{9fCkFb*8I>pwb)%1h!RG*!lVS1`^fF7>mz~Lm|&mJYdJH8PKz8RhOg}B
+x>ZsiV`!7Oys_KJcB7KM<3;Bi#h|psQ|MwFi;hki=h9|J+`$KnL_4gx@sDW=Vq^NXj{)N)*EdnuMuA;
+x|G^O2k9(GWrD;dF;)_PkS>CVr=WuWEyAMPDtdY%!R07po};cFS%bL8B-DRHP7B8-pOYBNx$u}Gpq61
+#*4w`)#wba-B|$=TWejXnay+Vmu%N4>W5ku{$El?`~Ib|p#nGfkqeWHRhbMRqL!j*m>1_BlV<9ziF&$ydjJj#8iX^#672D}m
+fop!j_mr6uB7di)RU4sgP0M}<|J^r&mH2|K!fo}z3ahSH-zxDN{;@n5D>{Yl}<_AFtiqQeXMc)sEr?t
+cgT(BZR4Q9?~T;1_NH5}bb=Fx4(=bq*=W3DMt*F}(%h|E@3DD~|i^1sQm16f2dP{t+-7#!R76y#iZI5
+=opWDrhMQ`U6o4XwQ1EipvRm*c6!rY5f>uoNYSjx5(uf1+Psc3w*9ug>*PwTi_>yop78?qe$i%BlGoJ
+sf#i<5P5`E0DUw%A~Uffi-vvdmh9O;y9*BhzM?JNyUr3jioajLx`@X1ExW-D88g&lE+fYc8xogNv|;I
+p)OIZ31lo4hEZBDzMo_NU_tgGny&?li_?y_N1lZS)pxNRsk^&TG}1(7r;%tdZX>PT
+{QqHFrG~r)pVmqQNYECNDPV-zZAMoGtFTM2!CsnJAl+@y)4GuC3k4isMf~+5%Y?geW#kjZT0v2{V2V)
+oeCOC@|aTeAaX3@Ds{ETrs_~en|+Y<2DeYen?D*>itT5%juA7R?xS)w)9+f;bnVpE;1>G=oaN2gKdODPpqTo_C>ZR0<|A78<+M%?
+I0;nMXX~(E7#B0ZMt|qTa#Ys?CU!%!rpunBUsA9Lg4l{KwZzhx%v
+Yf!^)MJ%^m{;_=(c0Jd+0B6SMc-h^@J~q*uwX%&0@DM>E3H3zHOxUJWtbhN}>G_Q`RYMd@dds*fn
+{<(srUBUE0q{yc{=YljH)Trp5ot~&H2S2hG`@ej)%#G&VvuSWMGsO6Hx^P5aE6RL=3?<
+Zfm^>Z%go+MzbWNPt!qRwLJbcozssBj$Xn5=zO1tw?p}*t$v
+NSD5u_vBrbC|H${WK`s6+qz1?@5>3?Ah?90!9!;|+&z0(|Pzv#?Bx;vfu-|rX5OfHUtE4N_S1M(iGJ3
+6R$9T!rB^A6vwgizyXEZ7@&P^2+W$kcDHwyuWy?I-TJf_46~{qr|pNQf;lA{}#69i~{I2Xqz;Xk3@gH
+1^W=6T?VRMLJ@iRHe+qcdefB&;rQpZSQ>)JN>di5-o=CKUDPMbPv$i_M-n7qMg90Ja;}BW<3`U{HIfX
+KBihXyhY+nR5O+glUkbx+MPMP#TM*W-2dJK!I!oEKbo?=dYTwwp=qD%@pKA9%(AAo(2>SE;Fmt2IeW~
+evMs=#S55)fhNYz^a#~SOs-@p0p&79xwb!t~X)f%4ZUmoWpyKZ}NTL4F$|KY(mj~`JmAdhY6F7lX0>n
+HT<0J@FoIVTMcxuwrtj76Wx8)qT*nvp=%k;GZWR&!4C$X$If^RU~I
+S9zIc3#JCvQN1K6J?hO{h+zCRw7q>0b78;3D=-cr^(Z6x*O4xO9mzwUpgJj5;3#x!m>}NGA@MqXHdr-
+(Z2vtO9KQH000080LuVbTkxXRe@YPm0O&XX03HAU0B~t=FJEbHbY*gGVQepBZ*FF3XLWL6bZKvHE^v9
+pTmNs{IF|q2e+5^?pf=V~ckUL48w0-G%p~mulO{#dc4iP5TE(JcZDmOiWvAY3?r*=(dn6@Nl9Oz)zy(
+EPOXTDG{*5m|5d2i+tMqPL#dTWbViV`_o!rR07E0FJax}UwHd55G+N61r6?gmD=t$)8MvjCyR^q-&>s
+sa_&Bc-diB+0O6=KmY&bGAbGyN8^QpGZjYnh1qv|fu?13403d0y1KUyVlQnFd?Nm6DZxmKDo5llEJo>?gHX8uYTrGEIvLU~KRn0Lq
+dekv~H{l|SN4T5D_hEbYhQyyob{JNASgS1=5V2lu+Y)`AIsHXkrZy~Usifxu$6!nktyoeK-Oh=QUbGC
+JwHAlo{nWU9ExGsb^%ec17e?7Z6x%~C|@Ny{EokR4Utk3ZToISW>ld6V)GFK!oU^K<&?PfH(itja@A6
+P(Q?#V0cz80^<^%{`Yah{BRN%I6749~{_eGjHW)zxG)`swoG?b*BN$K&hMi{tmFSQij>X`TimU0Fhf%
+#_q`=-gm*dht1;_1DqayNk=K(-Ydbla+!D06SRAtc3o(5+9+lUvQuj#mZM*McNFVjw>0C^pZJKtHu54
+`t)iZY(Wj+fu2u9*L5kE=_+0Fig)LiKOUdYgS)J_jWfOKdar7^5x=I1sD&o3^OWM|WT8O-;HgT5zLy{jN2ym8(T#2Z8!End)-;6a*9Q_ado-q&Mq
+u$3~r177m9x8?Wz_v=>#Z>~fvEHa+TP>v+ONX&V#ol>BevV)c>H-lAm3?#*g+6+@|PCEr1@0EDR%Vz@z|qa(q&kv$}FwNpnod9pBVN*1tZTbL>zO+%)
+BIB);Hv?rY#>0BQ?5atKI{a-848BDulmYhkq~9d`zFFqT}i&Vg((dS0oA(077yV7Aid$lhZSbvl#I&c
+L^PgbG_3L9Yd{A{WBRr2IXGvp90Y%Lw+qGRPu)7;D3Y3v{}ID^*wb+kK0upH1rE&4f6geOe|mMXooL`
+Ee8lRLVsw^cYV=pNOsfCQgT!?anoh>qPvb<9oF?ZI_(l>wmw4dmKhAz*Wbn;{R}pfE;+elH*keo4L(%
+G4q-;Y+62e)63j~dQWiNL0hl>3
+ok1@JceOb0wXQ+%$fi%N2g)yZowX-$#!09XrTnF?v8kYmYk;YEAjFK&U5VpzIHiK}hV5>pnb&iCC(g!zM1rh_T30JLpa
+(y`BD(_8T!iw0s9=?MHt>i}1WfzUA#J@uOVE6vyr<6_KT=t9bc^sL_QJ9^ZizjdpN}8;pwn5-373=d
+L5B(m8f;ysRAVUZ>*ohz~gz%9QrOfh-EB^+V$7A=1Q3q?y1A>i(akaN6_?p1Tn2-VF+H4;*coe*o2(s
+4iaxbJCzCsrX=)@h>5LC%=CrSV4C*7I~PbY_E%0bz8B@LI!XFPhVSEicMlqpin9w+px@x(u;dsetD;b
+7(1fi@XZ4|mTENa?8;%AEn_dL`O&i)9Utp?-y%5F5mMDrHvNOqWu^mQvIPLDs%0vwa6xa8V#;FW1E4p
+h+)cE_q`eE{OT5%3s&^HD@BrVuQpa33XNMLG2ZUo}icohr;*D#RsAJ!79XK;Ao&apn+4ZXfu-Wu;YEt
+*4$|1P&(!5;|ml|tBGk?t>c}H8dv2eKZCxkdAlOGQ&HO9)>EFyOQ_5a$0v5ZJFD;1*8Ib==_rNa60yd
+>w~6Jj8!@;L2Bsaij#Xriw!Pi~voVX~BJ%n@JJn5Wy;a=DnkWr0345zwUYpTR@D|lbhFnVD1
+(?8flP2@;7oqroAPHc|ZupLZqTKEFE)060dOqxqIdB3AeYhC7fZqq*+-_tH~x!buLB1vg{VfPqX
+il|IWn?TwWq~`8LZIgYH(VX}=kJ{Ut4V7J|q>a3>&=En7n0rUR!z1@GCpl!iSJ#1=Ilz)%rBDF3NJqv
+=d(KG;TxPj3^KXgRMNN}tYF`l_EsV64GgZKLcSd;iv$>ECqMU3htQ3ybUW2G!j3bPIy7`pMX!r}7%_7
+LPy7J1d6+i0_9Qd9wR@{hfL(#!dhAJ2kN}c5095x5ep)E_2g-D@VPzokD_{My0Fw&w@E?$wp+Gp;HyQQ;>oraOMI_obBCxwHG
+P`>eY~HN`&<`Udl?uyOHT?No^fcZY`^e+E%P?<(D$eD&RVvbwzo)(ngh-zt_}B))fn2{kka1cQ!Z`vt
+{XdO*bbT4vS51W=1F)bYdwpC)X}~jzeV7GNrv3iWpPtsSs3%qiG%6zr#!`5c_Qimf!axP;aW?fw?#3zKq|LY54|O{#L3tfBxaW{_`d2^w1RFaCQy4RFThDffAuF?TX+XGh-pg|6-QyJKvg9}H%IGz*
+^SP5OIp%!bAr4!F;*Yi!Qo*VLRd*qd9w_Fy;0b)k6JO>NXLjd*stv%}Y1ytz|n&STnTOB%L?W`j0$U_s7A>L*I1UGo6NZ*HYR={>`7KDNkN!I+m0g?XIvgtmVo8;<1<=7EJcW}^`#_xnl+cF@v9~?jEO-*&}s-2
+&yIWR4Bp>8Et3B0fc!nR0*os{#$s}&R)1{(9wH~=6UuF?d7pZ3xOI{V+cqRIOc*iu
+O?a48iTV*NZJYIBNFkim5WgUVF0k22PTyGK=>*r=fK;_ZNb*uX!Zx7VO<4QBpO6++C3u8}4ti_$%
+SR&~Hd`sk>2V$=`)8XdG9XrAh}%E6Ym2&rxzZ_cY6LG6q)6NQq+U@1Mu
+j@!n^D0vhU!fmkXbV`ENQ~o!biJ_Rnoz_<>1}T12UxsTiunpG!DsTEz^2m`*0oV!9GII5N|zQJutT@b
+;$<-O
+|G}1dyj1I_o)7bLC%90h5zx=i^7R(2cOKJb@V3Ey2YzkI;fkY?-$`zd
+(jWR`i38V%5JrEwcRBv_1={c$+Y{@7b}NR6E(FYIvHy234qsQngFYhS0q`ga?VsX2jvu-+SbwMc!JX>N37a&BR4FJ*XRWpH$9Z*FrgaCzN5+j8
+T`b?^F$HYTYdZ4$G)zLblJwyf1?cBXbNilkZFrBx^pXp*o%07iq-a2Zym@(oVqH}Vhp6Msq0rMuC%QP
+g;=QjThOL;~HXPoF-0?%nb{@9a}3^D@p-CDuwtqFff)da=ybrO1QuE?7wa=;&%0E3wL=bt1*PkC#{C{
+f8@278_A!B3|WLQHptytwfM+%4M7`#6yg~#cdwv{xnWYS)@U73(b$RToyqoeL*ncKlv$_VTmJWVkR}X
+U`8bN3ijo{f@jf4WslSrglDI%H6G!hv#U-?I#=N%mp%;|K#F4u*eP7U@
+!3OxcM!_kp`7G~)@UgSZcWZ^t3<)<=*fg?Q
+w+nuZvrsuqM>e~gt>Uj_8AL%Dn4aGRO1_L$S;Vt7QlhV4D$9qgxbvgzAx*L%vdF%I{qs^k?CT6z33#*h)JaO}m4G%KVx6u;#%Nf3B(U&txXuDB1jY{&ooTdktAqA6va4BkQg;H0y
+v=VP(4L2pj&{9F2#5(9+n`zL{LbBA?Q!!cG8T58q81+rm=*v001%sh!N-kJiJMyOBnbMWKNE-Sd#$p7
+z9`Y`I>u>ZUN1%a=`4<7*(qr&(?7Q{B`S_l`>4LCr=xOpV9t=O&OQdX~m05Ci9W~_VeoF9Wdy`sI6;{
+&;e%k9)cnTRJm+~gzYz?k@}{=&tmLZ;rv?3rW3CH2>uJ6L3wWH
+;2_+ck$4_H(_7Wgci^KR3W2H=v4zU(TB~Ec0OK+f6k0{&`Eyr&E%p)2VM%@NOcu^*Qgp_J;081k14i7
+}$u|3s`UB>?2Y-gW7`b8*bn=n%Z%X8+iPz*ukmDzHS%z$JNv*&NwPeIw~?sEojtROCHPH
+q?|RE{c3_Jo(aqMv*j>N4nTTuWX~k6V%4yG=ZiP
+(beUMr=Oq*k_eAC0#cBx|^)(5Px?X?)2KQ3pEI$MOz^>lxDm}7^`H~;frKt17gw`Wvp?f?3h|LEy3{7
+EKfY>%I0a)15L|NiS={{8Ex%@Cd*G{d?L{kP2dZ(dOE8ur{lOX-RI6KhY3C*