ENPAF — Python + Web framework for building Android APK applications
Project description
🚀 ENPAF — Engine for Native Python App Framework
Создавайте Android-приложения на Python + HTML/CSS/JS и собирайте их в APK.
ENPAF — это фреймворк, который позволяет писать мобильные приложения, используя привычный веб-стек (HTML/CSS/JS) для интерфейса и Python для логики. В режиме разработки приложение работает как обычный сайт (Flask + WebSocket + hot-reload), а готовый продукт собирается в настоящий APK на базе WebView + встроенного Python (Chaquopy).
┌──────────────────────── Android APK ────────────────────────┐
│ ┌────────────┐ Bridge (JSON) ┌────────────────────┐ │
│ │ WebView │ ◄───────────────► │ Python (main.py) │ │
│ │ HTML/CSS/JS│ │ enpaf core │ │
│ │ + enpaf.js │ │ ваш код │ │
│ └────────────┘ └──────────────────── │
└─────────────────────────────────────────────────────────────┘
📑 Содержание
- Возможности
- Установка
- Требования
- Быстрый старт
- Создание базового приложения с нуля
- CLI: команда
paf - Структура проекта
- Конфигурация
enpaf.json - Python API
- JavaScript SDK (
enpaf.js) - Датчики и сенсоры устройства
- NFC: чтение и запись меток
- Запрос разрешений во время работы (runtime)
- Панель настроек ⚙
- Разрешения и фичи устройства
- Deep links (диплинки)
- Уведомления и нативные возможности
- Сборка APK
- Что можно импортировать
- Решение проблем
- Публикация в PyPI (для мейнтейнеров)
- Лицензия
✨ Возможности
- 🐍 Python для логики, HTML/CSS/JS для интерфейса.
- 🔗 Двусторонний мост Python ↔ JavaScript (вызовы и события).
- 💾 Встроенное хранилище — key-value и коллекции на SQLite.
- ⚡ Hot-reload в режиме разработки.
- ⚙️ Веб-панель настроек — иконка, имя, ориентация, цвета, разрешения, фичи и диплинки настраиваются прямо в браузере и пишутся в
enpaf.json. - 📱 Нативные API — toast, вибрация, уведомления, буфер обмена, шаринг, ориентация.
- 🛰 Чтение датчиков из Python — геолокация, акселерометр, гироскоп, магнитометр, освещённость, NFC, Bluetooth, микрофон, батарея, сеть.
- 🔐 Разрешения по запросу — запрашивайте доступ в нужный момент прямой функцией
из Python (
app.api.request_permissions([...])), а не при запуске. - 🧩
uses-feature(камера, NFC, датчики, Bluetooth, GPS…). - 🔗 Deep links (кастомные схемы и App Links).
- 📦 Сборка в APK/AAB на Windows/macOS/Linux через Gradle + Chaquopy.
📥 Установка
Из PyPI
pip install enpaf
После установки доступна команда paf и пакет enpaf для импорта.
Из исходников (для разработки фреймворка)
git clone https://github.com/aaalllexxx/enpaf
cd enpaf
pip install -e .
📋 Требования
| Что | Зачем | Версия |
|---|---|---|
| Python | фреймворк и CLI | 3.9+ |
| Java JDK | сборка APK | 17–21 (рекомендуется 17) |
| Android SDK | сборка APK | Android Studio или command-line tools |
Для разработки (
paf run) нужен только Python. JDK и Android SDK нужны только для сборки APK (paf build). Проверить окружение:paf doctor.
⚡ Быстрый старт
paf create myapp # создать проект
cd myapp
paf run # запустить dev-сервер → http://127.0.0.1:8080
# ... разрабатываете, правите app/ и main.py, страница перезагружается сама ...
paf build apk # собрать APK → dist/myapp-1.0.0.apk
Установить APK на телефон:
adb install dist/myapp-1.0.0.apk
# либо просто перекиньте .apk на телефон и откройте
🧱 Создание базового приложения с нуля
Соберём небольшое приложение «Заметки + датчик»: кнопка зовёт Python, заметки хранятся в SQLite, а отдельная кнопка читает геолокацию с устройства (предварительно запросив разрешение). Все файлы — настоящие, можно копировать как есть.
Шаг 1. Создать проект
paf create myapp
cd myapp
Получится дерево (см. Структура проекта). Дальше правим четыре
файла: enpaf.json, main.py, app/index.html, app/js/app.js.
Шаг 2. enpaf.json — метаданные и разрешения
permissions лишь объявляют разрешения в манифесте; сам системный диалог мы
покажем позже из Python (см. Шаг 5).
{
"name": "MyApp",
"package": "com.example.myapp",
"version": "1.0.0",
"orientation": "portrait",
"permissions": ["INTERNET", "FINE_LOCATION", "VIBRATE"],
"features": [
{ "key": "GPS", "required": false }
],
"min_sdk": 24,
"target_sdk": 34,
"theme": { "primary_color": "#6C5CE7", "status_bar_color": "#5A4BD1" }
}
Шаг 3. app/index.html — интерфейс
enpaf.js подключать не нужно: dev-сервер и сборщик APK внедряют мост сами.
<!-- файл: app/index.html -->
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>MyApp</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<h1>MyApp</h1>
<button onclick="sayHello()">Поздороваться с Python</button>
<div id="greeting"></div>
<h2>Заметки</h2>
<input id="noteInput" placeholder="Текст заметки…">
<button onclick="addNote()">Добавить</button>
<ul id="notes"></ul>
<h2>Где я?</h2>
<button onclick="whereAmI()">Узнать геолокацию</button>
<div id="location"></div>
<!-- ваш код приложения -->
<script src="js/app.js"></script>
</body>
</html>
Шаг 4. app/js/app.js — фронтенд-логика
Весь обмен с Python идёт через глобальный объект window.enpaf (он готов после
загрузки страницы; дождаться можно через enpaf.ready(...)).
// файл: app/js/app.js
// Вызвать Python-функцию "hello" (зарегистрирована в main.py)
async function sayHello() {
const res = await enpaf.call("hello", { name: "Alex" });
document.getElementById("greeting").textContent = res.message;
}
// Сохранить заметку через Python -> SQLite
async function addNote() {
const input = document.getElementById("noteInput");
if (!input.value.trim()) return;
await enpaf.call("save_note", { text: input.value.trim() });
input.value = "";
loadNotes();
}
// Загрузить заметки из Python
async function loadNotes() {
const { notes } = await enpaf.call("get_notes", {});
document.getElementById("notes").innerHTML =
notes.map(n => `<li>${n.text}</li>`).join("");
}
// Запросить разрешение на геолокацию в нужный момент, затем прочитать датчик
async function whereAmI() {
const out = document.getElementById("location");
// Системный диалог появится именно сейчас, по нажатию кнопки:
const grant = await enpaf.permissions.request(["FINE_LOCATION"]);
if (grant.denied && grant.denied.length) {
out.textContent = "Доступ к геолокации не выдан";
return;
}
const loc = await enpaf.sensors.location();
out.textContent = loc.fix
? `Широта ${loc.latitude}, долгота ${loc.longitude}`
: "Координаты пока недоступны";
}
// Подгрузить заметки сразу после готовности моста
enpaf.ready(loadNotes);
Шаг 5. main.py — логика на Python
# файл: main.py
from enpaf import EnpafApp
app = EnpafApp(__name__)
# ─── Страница ────────────────────────────────────────────────
@app.route("/")
def index():
return app.render("index.html", title=app.name)
# ─── Bridge-функции (их зовёт app/js/app.js через enpaf.call) ─
@app.bridge_handler("hello")
def hello(params):
name = params.get("name", "World")
app.api.vibrate(150) # нативная вибрация
return {"message": f"Привет, {name}! 👋"}
@app.bridge_handler("save_note")
def save_note(params):
text = params.get("text", "").strip()
if not text:
return {"success": False}
note_id = app.storage.collection("notes").add({"text": text})
return {"success": True, "id": note_id}
@app.bridge_handler("get_notes")
def get_notes(params):
return {"notes": app.storage.collection("notes").all()}
# ─── Реакция на результат запроса разрешения (необязательно) ──
@app.on("permission_result")
def on_permission(data):
print("Выданы:", data["granted"], "| отклонены:", data["denied"])
if __name__ == "__main__":
app.run()
Шаг 6. Запустить и собрать
paf run # http://127.0.0.1:8080 — проверяем в браузере (hot-reload)
paf build apk # dist/myapp-1.0.0.apk — ставим на телефон
💡 В браузере (
paf run) датчики и разрешения возвращают dev-заглушки (например, фиксированные координаты), чтобы интерфейс можно было отладить без телефона. На устройстве из APK читаются реальные значения.
🛠 CLI: команда paf
| Команда | Описание |
|---|---|
paf create <name> |
Создать новый проект из шаблона |
paf run |
Запустить dev-сервер (Flask + hot-reload) |
paf build apk |
Собрать debug APK |
paf build apk --release |
Собрать release APK |
paf build aab |
Собрать release Android App Bundle (.aab) |
paf doctor |
Проверить окружение (Python, JDK, Android SDK) |
paf info |
Показать информацию о текущем проекте |
Флаги create:
--package,-p— Android package id (по умолчаниюcom.enpaf.<name>)--template,-t— шаблон проекта (по умолчаниюdefault)
Флаги run:
--host(по умолчанию127.0.0.1)--port(по умолчанию8080)--no-browser— не открывать браузер автоматически--debug— режим отладки
Флаги build:
--release— релизная сборка--keystore <path>— keystore для подписи--clean— чистая сборка (удалить кэш сборки)
🏗 Структура проекта
myapp/
├── app/ # Интерфейс (HTML/CSS/JS) → попадёт в assets APK
│ ├── index.html # Главная страница
│ ├── css/style.css
│ ├── js/app.js
│ ├── pages/ # Доп. страницы
│ └── img/
├── main.py # Python-логика (точка входа)
├── enpaf.json # Конфигурация проекта
├── icon.png # (опционально) иконка приложения
├── data/ # Локальная БД (SQLite) — создаётся автоматически
└── dist/ # Готовые APK после сборки
⚙️ Конфигурация enpaf.json
Полный пример со всеми полями:
{
"name": "My App",
"package": "com.example.myapp",
"version": "1.0.0",
"description": "My ENPAF Application",
"author": "Developer",
"orientation": "portrait",
"icon": "icon.png",
"permissions": ["INTERNET", "CAMERA", "VIBRATE"],
"features": [
{ "key": "CAMERA", "required": true },
{ "key": "NFC", "required": false }
],
"deeplinks": [
{ "label": "Open profile", "scheme": "myapp", "host": "open",
"path": "/profile", "pathType": "prefix", "autoVerify": false }
],
"python_requirements": ["requests"],
"min_sdk": 24,
"target_sdk": 34,
"theme": {
"primary_color": "#6C5CE7",
"status_bar_color": "#5A4BD1"
}
}
| Поле | Тип | Описание |
|---|---|---|
name |
string | Название приложения (ярлык на устройстве) |
package |
string | Android application id, напр. com.example.myapp |
version |
string | Версия (major.minor.patch) |
description, author |
string | Метаданные |
orientation |
string | portrait / landscape / auto / sensor / unspecified |
icon |
string | Путь к иконке (PNG/JPG/WebP) относительно проекта |
permissions |
string[] | Список ключей разрешений (см. таблицу) |
features |
object[] | `{ "key": "...", "required": true |
deeplinks |
object[] | Диплинки (см. раздел) |
python_requirements |
string[] | pip-зависимости, встраиваемые в APK через Chaquopy |
min_sdk / target_sdk |
int | Android API levels (по умолчанию 24 / 34) |
theme.primary_color |
string | Основной цвет (HEX) |
theme.status_bar_color |
string | Цвет статус-бара (HEX) |
log_level |
string | Уровень логирования (INFO, DEBUG, …) |
💡 Эти поля удобнее всего редактировать через панель настроек ⚙, не открывая JSON вручную.
🐍 Python API
main.py — точка входа
from enpaf import EnpafApp
app = EnpafApp(__name__)
# ─── Маршруты страниц (Jinja2-шаблоны, режим разработки) ───
@app.route("/")
def index():
return app.render("index.html", title=app.name)
# ─── Bridge-функции (вызываются из JavaScript) ───
@app.bridge_handler("get_user")
def get_user(params):
user_id = params.get("id")
return {"id": user_id, "name": "Alex"}
# ─── События жизненного цикла ───
@app.on("app_start")
def on_start():
print("Приложение запущено!")
if __name__ == "__main__":
app.run()
Класс EnpafApp
EnpafApp(import_name, app_dir="app", config_file="enpaf.json")
Декораторы / методы:
| Метод | Назначение |
|---|---|
@app.route(path, methods=None) |
Зарегистрировать страницу-маршрут |
@app.bridge_handler(name) / @app.bridge_func(name) |
Зарегистрировать функцию, вызываемую из JS |
@app.on(event) |
Подписаться на событие |
app.emit(event, data=None) |
Отправить событие (в Python и в JS) |
app.render(template, **context) |
Отрендерить Jinja2-шаблон из app/ |
app.run(host, port, debug, open_browser) |
Запустить (dev-сервер или Android runtime) |
Свойства / компоненты:
| Свойство | Тип | Описание |
|---|---|---|
app.config |
dict | Содержимое enpaf.json |
app.name |
str | Имя приложения |
app.storage |
Storage |
Локальное хранилище |
app.events |
EventEmitter |
Система событий |
app.bridge |
Bridge |
Мост Python↔JS |
app.api |
DeviceAPI |
Доступ к функциям устройства |
app.router |
Router |
Роутер/шаблонизатор |
Хранилище — app.storage
Key-value:
app.storage.set("theme", "dark") # значение (str/int/float/bool/dict/list)
theme = app.storage.get("theme", "light") # с дефолтом
app.storage.delete("theme")
app.storage.exists("theme") # -> bool
app.storage.keys("user_%") # LIKE-паттерн
app.storage.all() # -> dict всех пар
app.storage.clear()
Коллекции (мини документ-стор):
notes = app.storage.collection("notes")
note_id = notes.add({"text": "Привет"}) # -> int (id)
notes.all() # -> list[dict] (+ _id, _created_at)
notes.find({"text": "Привет"}) # -> list[dict]
notes.find_one({"text": "Привет"}) # -> dict | None
notes.update(note_id, {"text": "Пока"})
notes.delete(note_id)
notes.count() # -> int
notes.clear()
На Android БД автоматически пишется в записываемый каталог приложения (
getFilesDir()), а не рядом с исходниками.
События — app.events
@app.on("app_start")
def _(): ...
app.events.once("page_load", handler) # сработает один раз
app.events.off("app_start", handler) # отписаться
app.events.emit("my_event", payload) # вызвать
Встроенные lifecycle-события: app_start, app_stop, app_pause,
app_resume, app_error, page_load, page_unload, bridge_connect,
bridge_disconnect.
Функции устройства — app.api (DeviceAPI)
app.api.toast("Сохранено!", duration="short") # "short" | "long"
app.api.vibrate(200) # мс
app.api.get_device_info() # -> dict
app.api.set_status_bar_color("#000000")
app.api.set_orientation("portrait") # "portrait"|"landscape"|"auto"
app.api.open_url("https://example.com")
app.api.clipboard_set("текст")
app.api.clipboard_get()
app.api.share("Посмотри!", title="Моё приложение")
Датчики устройства — app.api (читаются из Python)
Каждый метод возвращает обычный dict (можно сразу return из bridge-функции).
В режиме paf run (браузер) возвращается заглушка с полем "dev": true.
# файл: main.py
@app.bridge_handler("read_sensors")
def read_sensors(params):
return {
"gyro": app.api.read_sensor("gyroscope"), # {values:[x,y,z], ...}
"accel": app.api.read_sensor("accelerometer"),
"location": app.api.get_location(), # {latitude, longitude, accuracy, ...}
"bt": app.api.get_bluetooth(), # {enabled, name, bonded:[...]}
"nfc": app.api.get_nfc(), # {present, enabled}
"mic": app.api.get_audio_level(), # {amplitude, db} (нужен RECORD_AUDIO)
"battery": app.api.get_battery(), # {level, charging}
"network": app.api.get_network(), # {connected, type}
}
| Метод | Возвращает |
|---|---|
app.api.read_sensor(name, timeout=2.0) |
Снимок датчика: accelerometer, gyroscope, magnetometer, light, proximity, pressure, gravity, rotation_vector, step_counter, heart_rate, … |
app.api.list_sensors() |
Список всех датчиков устройства |
app.api.get_location() |
Последняя известная геопозиция |
app.api.get_bluetooth() |
Состояние Bluetooth + сопряжённые устройства |
app.api.get_nfc() |
Наличие/состояние NFC |
app.api.get_audio_level(duration=0.4) |
Пиковая громкость с микрофона |
app.api.get_battery() |
Уровень заряда и зарядка |
app.api.get_network() |
Тип подключения к сети |
app.api.get_sensor_snapshot() |
Всё самое частое одним вызовом |
Разрешения по запросу — app.api
# файл: main.py
@app.bridge_handler("enable_mic")
def enable_mic(params):
# Показать системный диалог именно сейчас (не при запуске приложения).
# Результат придёт в @app.on("permission_result") и в JS-событие.
app.api.request_permissions(["RECORD_AUDIO"])
return {"requested": True}
@app.bridge_handler("mic_ready")
def mic_ready(params):
return app.api.check_permission("RECORD_AUDIO") # {granted: bool}
Подробнее — в разделе Запрос разрешений во время работы.
🌐 JavaScript SDK (enpaf.js)
Мост enpaf.js подключается автоматически: в режиме разработки его внедряет
сервер, а при сборке APK — билдер вставляет <script src="js/enpaf.js"> в ваши
HTML-страницы. Глобальный объект — window.enpaf.
Вызовы и события
// Вызвать Python-функцию (-> Promise)
const user = await enpaf.call("get_user", { id: 42 });
// События из Python
enpaf.on("data_updated", (payload) => console.log(payload));
enpaf.off("data_updated");
// Отправить событие в Python
enpaf.emit("button_clicked", { id: "save" });
// Готовность моста
enpaf.ready(() => console.log("bridge ready"));
// Навигация между страницами
enpaf.navigate("/pages/about.html");
enpaf.version; // "1.0.0"
enpaf.isAndroid; // true в APK, false в браузере
Хранилище из JS
await enpaf.storage.set("theme", "dark");
const theme = await enpaf.storage.get("theme");
await enpaf.storage.delete("theme");
Функции устройства — enpaf.device
| Метод | Описание |
|---|---|
enpaf.device.toast(msg, dur) |
Toast-уведомление ("short"/"long") |
enpaf.device.vibrate(ms) |
Вибрация |
enpaf.device.notify(title, text, id) |
Системное уведомление |
enpaf.device.share(text, title) |
Системный «Поделиться» |
enpaf.device.setOrientation(mode) |
"portrait"/"landscape"/"auto" |
enpaf.device.clipboard(text) |
Скопировать в буфер обмена |
enpaf.device.openUrl(url) |
Открыть ссылку во внешнем браузере |
enpaf.device.getInfo() |
Информация об окружении (Promise) |
В браузере (dev) методы используют веб-аналоги (Web Notifications, navigator.share,
navigator.clipboard и т.д.), на Android — нативные вызовы.
Утилиты
enpaf.utils.formatDate(Date.now(), "ru-RU");
enpaf.utils.uid(); // случайный id
🛰 Датчики и сенсоры устройства
Датчики читаются в Python (app.api.*),
а из интерфейса удобнее всего звать их через enpaf.sensors.* — каждый метод
возвращает Promise с тем же dict, что и Python.
// файл: app/js/app.js
const gyro = await enpaf.sensors.read("gyroscope"); // {values:[x,y,z], accuracy, ...}
const loc = await enpaf.sensors.location(); // {latitude, longitude, accuracy, fix}
const bt = await enpaf.sensors.bluetooth(); // {enabled, name, bonded:[...]}
const nfc = await enpaf.sensors.nfc(); // {present, enabled}
const mic = await enpaf.sensors.audioLevel(); // {amplitude, db}
const batt = await enpaf.sensors.battery(); // {level, charging}
const net = await enpaf.sensors.network(); // {connected, type}
const all = await enpaf.sensors.snapshot(); // всё одним вызовом
const list = await enpaf.sensors.list(); // список датчиков устройства
| JS-метод | Python-эквивалент |
|---|---|
enpaf.sensors.read(name, opts?) |
app.api.read_sensor(name) |
enpaf.sensors.list() |
app.api.list_sensors() |
enpaf.sensors.location() |
app.api.get_location() |
enpaf.sensors.bluetooth() |
app.api.get_bluetooth() |
enpaf.sensors.nfc() |
app.api.get_nfc() |
enpaf.sensors.audioLevel(dur?) |
app.api.get_audio_level() |
enpaf.sensors.battery() |
app.api.get_battery() |
enpaf.sensors.network() |
app.api.get_network() |
enpaf.sensors.snapshot() |
app.api.get_sensor_snapshot() |
Какие нужны разрешения: геолокация — FINE_LOCATION/COARSE_LOCATION;
микрофон — RECORD_AUDIO; сопряжённые Bluetooth-устройства (Android 12+) —
BLUETOOTH_CONNECT; NFC — NFC. Добавьте их в enpaf.json и запросите во
время работы (следующий раздел). Акселерометр, гироскоп, освещённость и т.п.
runtime-разрешений не требуют.
В браузере (
paf run) методы возвращают правдоподобные dev-заглушки ({ "dev": true, ... }), на устройстве — реальные показания.
🏷 NFC: чтение и запись меток
ENPAF включает foreground dispatch: пока приложение открыто, поднесённая
метка автоматически попадает в него (событие nfc_tag), а дальше её можно
прочитать, перезаписать или заблокировать из Python или JS. Добавьте
разрешение NFC в enpaf.json.
Чтение
// файл: app/js/app.js
enpaf.nfc.onTag(async (tag) => { // метку поднесли к телефону
const data = await enpaf.nfc.read();
// data.records: [{type:"text", text:"…"}] | [{type:"uri", uri:"…"}] | [{type:"raw", …}]
console.log("ID:", data.id, "записи:", data.records);
});
Запись — все типы меток
⚠️ Важно: дескриптор метки «умирает», как только метку убрали от телефона. Поэтому надёжный способ —
arm*-методы: они откладывают запись и выполняют её в момент следующего касания. Сначала нажмите запись → потом поднесите метку.Promiseзавершится результатом записи.
// файл: app/js/app.js — РЕКОМЕНДУЕТСЯ (arm → поднести метку)
const r = await enpaf.nfc.armUri("https://enpaf.dev"); // ждёт касания метки
// r = {written: true, bytes: N} либо {written:false, note:"…"}
await enpaf.nfc.armText("Привет, NFC");
await enpaf.nfc.armWifi("МояСеть", "пароль123");
await enpaf.nfc.armApp("com.example.myapp");
await enpaf.nfc.armContact({ name: "Alex", phone: "+7999" });
await enpaf.nfc.armLock(); // заблокировать след. метку
Прямые write*-методы (ниже) пишут в уже поднесённую метку — годятся внутри
обработчика enpaf.nfc.onTag(...), когда метка точно в поле:
// файл: app/js/app.js
await enpaf.nfc.writeText("Привет, NFC"); // текст
await enpaf.nfc.writeUri("https://enpaf.dev"); // ссылка (URL)
await enpaf.nfc.writeUri("tel:+79991234567"); // телефон
await enpaf.nfc.writeUri("mailto:hi@example.com"); // email
await enpaf.nfc.writeUri("geo:55.75,37.61"); // координаты
await enpaf.nfc.writeApp("com.android.chrome"); // запуск приложения (AAR)
await enpaf.nfc.writeWifi("МояСеть", "пароль123"); // Wi-Fi (tap-to-connect)
await enpaf.nfc.writeContact({ name: "Alex", phone: "+7999", email: "a@b.c" }); // контакт (vCard)
await enpaf.nfc.writeMime("application/json", '{"id":42}'); // MIME
// Несколько записей в одном сообщении:
await enpaf.nfc.writeRecords([
{ kind: "uri", uri: "https://enpaf.dev" },
{ kind: "app", package: "com.example.myapp" },
]);
То же из Python:
# файл: main.py
@app.bridge_handler("write_card")
def write_card(params):
app.api.nfc_write_text("Привет")
app.api.nfc_write_uri("https://enpaf.dev")
app.api.nfc_write_app("com.example.myapp")
app.api.nfc_write_wifi("МояСеть", "пароль123")
return {"ok": True}
Метод (enpaf.nfc.* / app.api.*) |
Тип NDEF-записи |
|---|---|
writeText(text) / nfc_write_text |
Текст (RTD_TEXT) |
writeUri(uri) / nfc_write_uri |
URL / tel: / mailto: / geo: / sms: |
writeApp(pkg) / nfc_write_app |
Запуск/установка приложения (AAR) |
writeMime(mime,data) / nfc_write_mime |
MIME (json, vcard, …) |
writeWifi(ssid,pass) / nfc_write_wifi |
Wi-Fi (vnd.wfa.wsc) |
writeContact({…}) / nfc_write_contact |
Контакт (vCard) |
writeRecords([…]) / nfc_write_records |
Любой набор записей |
read() / nfc_read |
Прочитать содержимое метки |
lock() / nfc_make_readonly |
Заблокировать метку навсегда (только чтение) |
Пустые/неформатированные метки форматируются автоматически.
lock()необратим — метку больше нельзя будет перезаписать.
🔓 Запрос разрешений во время работы (runtime)
«Опасные» разрешения (камера, геолокация, микрофон, контакты…) недостаточно
объявить в enpaf.json — Android требует согласия пользователя в рантайме.
ENPAF не показывает эти диалоги при запуске: вы вызываете их сами, в нужный
момент — например, когда пользователь впервые жмёт «Записать аудио».
Из Python (main.py)
# файл: main.py
@app.bridge_handler("start_recording")
def start_recording(params):
# Покажет системный диалог сейчас. Возвращает то, что уже выдано/запрошено.
res = app.api.request_permissions(["RECORD_AUDIO"])
return res # {"requested":[...], "granted":[...], "pending": true|false}
# Итоговый ответ пользователя приходит сюда (и одновременно — в JS-событие):
@app.on("permission_result")
def on_permission_result(data):
if "android.permission.RECORD_AUDIO" in data["granted"]:
print("Микрофон разрешён — можно писать звук")
else:
print("Отклонено:", data["denied"])
| Метод Python | Назначение |
|---|---|
app.api.request_permissions([...]) |
Показать системный диалог сейчас |
app.api.check_permission("CAMERA") |
{granted: bool} — выдано ли одно |
app.api.check_permissions([...]) |
Статус сразу нескольких |
Имена можно писать коротко ("CAMERA", "FINE_LOCATION", "RECORD_AUDIO") или
полностью ("android.permission.CAMERA").
Из JavaScript (app/js/app.js)
enpaf.permissions.request(...) возвращает Promise, который ждёт ответа
пользователя и резолвится итогом — это самый удобный путь из интерфейса:
// файл: app/js/app.js
async function recordAudio() {
const r = await enpaf.permissions.request(["RECORD_AUDIO"]);
if (r.granted.includes("android.permission.RECORD_AUDIO")) {
const level = await enpaf.sensors.audioLevel();
console.log("Громкость:", level.db, "дБ");
} else {
enpaf.device.toast("Нужен доступ к микрофону");
}
}
// Уже выдано?
const cam = await enpaf.permissions.check("CAMERA"); // {granted: bool}
const many = await enpaf.permissions.checkAll(["CAMERA", "RECORD_AUDIO"]);
| JS-метод | Назначение |
|---|---|
enpaf.permissions.request(list) |
Диалог + Promise с результатом {granted, denied, results} |
enpaf.permissions.check(name) |
{granted: bool} |
enpaf.permissions.checkAll(list) |
{granted:[...], denied:[...]} |
⚠️ Запрос разрешений работает только в собранном APK. Изменения в
main.pyприменяются в новой сборке (paf build apk) — переустановите APK.
⚙️ Панель настроек ⚙
Запустите paf run и откройте http://127.0.0.1:8080/enpaf-settings (или нажмите
плавающую кнопку ⚙ в правом нижнем углу страницы). Панель позволяет настроить и
сохранить в enpaf.json без ручного редактирования:
- General — имя приложения, иконка (загрузка с превью), ориентация, основной цвет и цвет статус-бара;
- Permissions — разрешения (
<uses-permission>); - Hardware features — фичи устройства (
<uses-feature>) с флагом «required»; - Deep Links — диплинки и App Links;
- Manifest preview — живой предпросмотр того, что попадёт в
AndroidManifest.xml.
После «Save» изменения применятся при следующей paf build.
🔐 Разрешения и фичи устройства
Разрешения
Указываются по коротким ключам в permissions (это лишь объявление в манифесте).
Доступные ключи:
INTERNET, ACCESS_NETWORK_STATE, ACCESS_WIFI_STATE, VIBRATE, CAMERA,
READ_STORAGE, WRITE_STORAGE, READ_MEDIA_IMAGES, READ_MEDIA_VIDEO,
READ_MEDIA_AUDIO, FINE_LOCATION, COARSE_LOCATION, BACKGROUND_LOCATION,
RECORD_AUDIO, BODY_SENSORS, ACTIVITY_RECOGNITION, READ_CONTACTS,
CALL_PHONE, READ_PHONE_STATE, SEND_SMS, RECEIVE_SMS, BLUETOOTH,
BLUETOOTH_ADMIN, BLUETOOTH_SCAN, BLUETOOTH_CONNECT, NFC, WAKE_LOCK,
FOREGROUND_SERVICE, POST_NOTIFICATIONS.
Можно указать и полное имя, напр. android.permission.CAMERA.
🔓 «Опасные» разрешения (камера, геолокация, микрофон, контакты, Bluetooth-скан, уведомления…) надо ещё и запросить в рантайме —
app.api.request_permissions([...])илиenpaf.permissions.request([...]). См. Запрос разрешений во время работы.
Фичи (<uses-feature>)
Формат: { "key": "<KEY>", "required": true|false }. Доступные ключи:
CAMERA, CAMERA_FRONT, CAMERA_AUTOFOCUS, NFC, BLUETOOTH, BLUETOOTH_LE,
GPS, LOCATION, MICROPHONE, WIFI, TELEPHONY, TOUCHSCREEN, FINGERPRINT,
ACCELEROMETER, GYROSCOPE, COMPASS, PROXIMITY, LIGHT, BAROMETER,
STEP_COUNTER, HEART_RATE.
required: falseоставляет приложение устанавливаемым на устройствах без соответствующего железа.
🔗 Deep links (диплинки)
Каждый диплинк превращается в <intent-filter> на главной активности.
"deeplinks": [
{ "label": "Профиль", "scheme": "myapp", "host": "open",
"path": "/profile", "pathType": "prefix", "autoVerify": false }
]
| Поле | Описание |
|---|---|
scheme |
Обязательно. Напр. myapp или https |
host |
Опционально. Напр. example.com |
path |
Опционально. Напр. /profile |
pathType |
path (точно) / prefix / pattern |
autoVerify |
true для App Links (проверяемые https-ссылки) |
label |
Только для UI-панели |
Проверить диплинк на устройстве:
adb shell am start -a android.intent.action.VIEW -d "myapp://open/profile"
🔔 Уведомления и нативные возможности
Базовые уведомления (JavaScript)
Из JavaScript:
enpaf.device.notify("Заголовок", "Текст уведомления", 1);
Rich-уведомления (Python)
Из Python можно отправлять мощные системные уведомления с картинками и кнопками:
import base64
with open("app/img/logo.png", "rb") as f:
img_b64 = base64.b64encode(f.read()).decode("utf-8")
app.api.notify(
title="Новое сообщение",
text="Привет!",
notification_id=1,
image_base64=img_b64, # Показывает большую картинку
action="open_chat", # Передается при клике на уведомление
payload="user_123",
buttons=[
{"text": "Ответить", "action": "reply"},
{"text": "Закрыть", "action": "close"}
]
)
Нажатие на само уведомление или любую его кнопку автоматически разбудит/откроет приложение и сгенерирует событие в Python:
@app.on("notification_click")
def on_notif_click(data):
print(f"Action: {data.get('action')}, Payload: {data.get('payload')}")
Важно: На Android 13+ приложение запрашивает разрешение
POST_NOTIFICATIONS(добавьте его вpermissionsв вашемenpaf.json).
Доступные нативные методы моста (вызываются через enpaf.device.* в JS или app.api.* в Python): toast, vibrate, notify, share, setOrientation, clipboard, openUrl.
📦 Сборка APK
paf build apk # debug
paf build apk --release # release
paf build aab # release bundle (.aab)
Что происходит:
- Проверяется окружение (Python/JDK/Android SDK).
- Подбирается совместимая JDK 17–21 (учитывается
JAVA_HOME; иначе ищется в стандартных местах, включая JBR из Android Studio). - Генерируется Gradle-проект (Chaquopy), скачивается официальный Gradle wrapper.
- Gradle собирает APK; результат копируется в
dist/<name>-<version>.apk.
Первая сборка долгая — Gradle докачивает Android platform/build-tools и NDK
(~1 ГБ) для Chaquopy. Дальнейшие сборки быстрее (всё кэшируется в ~/.gradle и
Android SDK).
Подпись release-сборки
Android не устанавливает неподписанные release-APK. ENPAF подписывает их
автоматически: при первой paf build apk --release создаётся keystore
~/.enpaf/keystores/<package>.jks (общий для всех последующих сборок этого
пакета — подпись стабильна, обновления ставятся поверх). Release собирается без
обфускации (minifyEnabled false), чтобы R8 не вырезал мост @JavascriptInterface
и классы Chaquopy.
Свой keystore (для публикации в Google Play) — через --keystore или блок
signing в enpaf.json:
"signing": {
"keystore": "release.jks",
"store_password": "••••••",
"key_alias": "myapp",
"key_password": "••••••"
}
paf build apk --release --keystore release.jks
OneDrive / облачные папки
Если проект лежит в OneDrive (или другой синхронизируемой папке), ENPAF
автоматически выносит каталог сборки в %LOCALAPPDATA%\enpaf\builds\…, потому что
синхронизация ломает удаление файлов Gradle. Путь можно переопределить переменной
окружения ENPAF_BUILD_DIR. Итоговый APK всё равно копируется в dist/.
📥 Что можно импортировать
Основное (рекомендуется):
from enpaf import EnpafApp, __version__
Продвинутое (обычно используется через app.*, но доступно напрямую):
from enpaf.core.storage import Storage, Collection
from enpaf.core.events import EventEmitter
from enpaf.core.bridge import Bridge
from enpaf.core.router import Router
from enpaf.core.api import DeviceAPI
# Справочники для манифеста / панели настроек
from enpaf.android.permissions import PERMISSIONS, get_permission_catalog
from enpaf.android.features import FEATURES, get_feature_catalog
from enpaf.android.deeplinks import get_deeplink_xml
# Программная сборка APK
from enpaf.builder.apk_builder import APKBuilder
# Точка входа CLI
from enpaf.cli.main import main
Клиентский SDK для подключения вручную (если не используете авто-инъекцию):
<script src="js/enpaf.js"></script>
🩺 Решение проблем
| Симптом | Причина и решение |
|---|---|
| Приложение сразу закрывается | Пересоберите APK — фиксы применяются только в новой сборке. Если повторяется — снимите лог: adb logcat (теги AndroidRuntime, python.stderr, chaquopy). |
Bridge call failed / Unexpected token '<' в paf run |
Старый билд enpaf: HTTP-фолбэк моста и Socket.IO-клиент чинятся в свежей версии. Обновите пакет (pip install -U enpaf или pip install -e . из исходников) и перезапустите paf run. |
enpaf is not defined |
Мост не подключён. Сборка вставляет js/enpaf.js автоматически; убедитесь, что собираете свежую версию. |
Датчик/геолокация возвращает {"dev": true} |
Это нормально в браузере (paf run) — реальные показания доступны только в APK на устройстве. |
request_permissions ничего не показывает |
Работает только в APK и только для «опасных» разрешений, которые объявлены в enpaf.json. Проверьте, что разрешение есть в permissions. |
| Синий/неожиданный цвет статус-бара | Это theme.status_bar_color. Поменяйте его в панели ⚙ → General или в enpaf.json. |
Unable to delete directory … python\sources |
OneDrive блокирует файлы. ENPAF собирает вне OneDrive автоматически; при желании задайте ENPAF_BUILD_DIR. |
Incompatible Java version |
Нужна JDK 17–21. Установите JDK 17 и задайте JAVA_HOME, либо дайте ENPAF найти её автоматически. |
| release-APK не устанавливается | Неподписанный release Android отклоняет. ENPAF подписывает release автоматически (keystore в ~/.enpaf/keystores/). Обновите пакет и пересоберите paf build apk --release. Если меняли keystore — сначала удалите старую версию приложения (конфликт подписи). |
keytool not found при release |
keystore создаётся через keytool из JDK. Убедитесь, что JDK 17 установлен и найден (paf doctor). |
'""' is not recognized при Gradle |
Старый сломанный wrapper. Удалите каталог сборки и соберите заново (paf build apk --clean). |
Полная диагностика окружения: paf doctor.
📤 Публикация в PyPI (для мейнтейнеров)
Пакет уже подготовлен (pyproject.toml, MANIFEST.in). Подробная пошаговая
инструкция — в PUBLISHING.md. Кратко:
pip install build twine
python -m build # создаст dist/*.whl и dist/*.tar.gz
twine check dist/*
twine upload --repository testpypi dist/* # сначала на TestPyPI
twine upload dist/* # затем на PyPI
📄 Лицензия
MIT License — см. LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file enpaf-1.0.6.tar.gz.
File metadata
- Download URL: enpaf-1.0.6.tar.gz
- Upload date:
- Size: 142.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4e7186214f5fc83988de27ce0c1c981cdf11af2c5f5bc5496ad698e9dfce92b1
|
|
| MD5 |
6701e8db432f396ead75f6df9d23357e
|
|
| BLAKE2b-256 |
9874fc752fed9ceb8001d371df93a590fac2c4f279c068343d7758a4e8496c94
|
File details
Details for the file enpaf-1.0.6-py3-none-any.whl.
File metadata
- Download URL: enpaf-1.0.6-py3-none-any.whl
- Upload date:
- Size: 122.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
83f3b3aa99e863f2c719d512781b0300b60684872dcb5a5b8f27595b02c112af
|
|
| MD5 |
b46d823e42d8010ced7733a48ef18b16
|
|
| BLAKE2b-256 |
f8c3fc73918cdd962c72d6f13a80cced4c027dcc1b4a6ff00cc6b3c3ce3a4f85
|