From a23c88f5b98799cf774cc9f6dc3060c559db528e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=90=D0=B1=D0=B0=D0=B9?= =?UTF-8?q?=D0=B4=D1=83=D0=BB=D0=B8=D0=BD?= Date: Wed, 26 Nov 2025 07:18:11 +0300 Subject: [PATCH] Initial commit --- .env.example | 6 + .gitignore | 33 +++ LICENSE | 21 ++ README.md | 343 ++++++++++++++++++++++++++++++ __init__.py | 8 + example.py | 240 +++++++++++++++++++++ requirements.txt | 2 + setup.py | 39 ++++ streamlit_supabase_project.tar.gz | Bin 0 -> 6175 bytes supabase.py | 212 ++++++++++++++++++ 10 files changed, 904 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 __init__.py create mode 100644 example.py create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 streamlit_supabase_project.tar.gz create mode 100644 supabase.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d04c5e1 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +# Supabase Configuration +# Получите эти данные из вашего проекта Supabase: +# Settings -> API -> Project URL и Project API keys + +SUPABASE_URL=your_supabase_project_url +SUPABASE_KEY=your_supabase_anon_key diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9e85c34 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Environment variables +.env + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Streamlit +.streamlit/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Distribution / packaging +dist/ +build/ +*.egg-info/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..85f53f0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 [Your Name] + +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/README.md b/README.md new file mode 100644 index 0000000..ba385c1 --- /dev/null +++ b/README.md @@ -0,0 +1,343 @@ +# Supabase Connector + +Простой и удобный Python коннектор для работы с [Supabase](https://supabase.com). + +## 🎯 О проекте + +Этот проект предоставляет удобную обертку над официальным Python клиентом Supabase, упрощая базовые операции с базой данных и Storage. + +## ✨ Возможности + +- ✅ Простое подключение к Supabase через переменные окружения +- 📊 CRUD операции (Create, Read, Update, Delete) +- 🔍 Фильтрация и лимитирование результатов +- 📦 Работа с Supabase Storage +- 🛡️ Обработка ошибок +- 🎨 Чистый и понятный API + +## 📋 Требования + +- Python 3.8+ +- Аккаунт Supabase (бесплатный или платный) + +## 🚀 Установка + +### Вариант 1: Клонирование репозитория + +```bash +git clone https://github.com/yourusername/supabase-connector.git +cd supabase-connector +pip install -r requirements.txt +``` + +### Вариант 2: Установка как пакет (в разработке) + +```bash +pip install -e . +``` + +## ⚙️ Настройка + +1. Создайте файл `.env` в корне проекта: + +```bash +cp .env.example .env +``` + +2. Заполните данными из вашего Supabase проекта: + +```env +SUPABASE_URL=https://your-project-id.supabase.co +SUPABASE_KEY=your_anon_key +``` + +**Где найти эти данные:** +1. Откройте [Supabase Dashboard](https://app.supabase.com) +2. Выберите ваш проект +3. Перейдите в Settings → API +4. Скопируйте **Project URL** и **anon/public key** + +## 📖 Использование + +### Базовый пример + +```python +from supabase import SupabaseManager + +# Инициализация +sb = SupabaseManager() + +# Получение данных +users = sb.select(table="users", limit=10) +print(f"Найдено пользователей: {len(users)}") + +# Добавление записи +new_user = sb.insert( + table="users", + data={ + "name": "Тимур", + "email": "timur@example.com", + "age": 28 + } +) +print(f"Создан пользователь с ID: {new_user.get('id')}") +``` + +### Фильтрация данных + +```python +# Получить пользователей с определенным возрастом +users_30 = sb.select( + table="users", + filters={"age": 30} +) + +# Получить только имена и email +users = sb.select( + table="users", + columns="name, email", + limit=5 +) +``` + +### Обновление записей + +```python +# Обновить возраст пользователя с ID=1 +updated = sb.update( + table="users", + data={"age": 31}, + filters={"id": 1} +) +``` + +### Удаление записей + +```python +# Удалить пользователя с ID=1 +deleted = sb.delete( + table="users", + filters={"id": 1} +) +``` + +### Работа с Storage + +```python +# Загрузить файл +with open("avatar.png", "rb") as f: + file_data = f.read() + +sb.upload_file( + bucket="avatars", + file_path="user_123/avatar.png", + file_data=file_data, + content_type="image/png" +) + +# Получить публичный URL +url = sb.get_public_url( + bucket="avatars", + file_path="user_123/avatar.png" +) +print(f"Файл доступен по адресу: {url}") +``` + +### Продвинутые запросы + +Для сложных запросов используйте прямой доступ к клиенту: + +```python +client = sb.get_client() + +# Запрос с операторами сравнения +adults = client.table("users")\ + .select("*")\ + .gte("age", 18)\ + .order("age", desc=True)\ + .execute() + +# Поиск по паттерну +ivan_users = client.table("users")\ + .select("*")\ + .ilike("name", "%иван%")\ + .execute() +``` + +## 📁 Структура проекта + +``` +supabase-connector/ +├── supabase.py # Основной класс SupabaseManager +├── __init__.py # Инициализация пакета +├── example.py # Примеры использования +├── requirements.txt # Зависимости +├── setup.py # Настройка пакета +├── .env.example # Шаблон конфигурации +├── .gitignore # Исключения для git +└── README.md # Документация +``` + +## 🧪 Запуск примеров + +В файле `example.py` находятся готовые примеры использования: + +```bash +python example.py +``` + +Раскомментируйте нужные примеры в функции `main()`. + +## 📝 API Reference + +### SupabaseManager + +#### `__init__()` +Инициализирует подключение к Supabase, используя переменные окружения. + +#### `select(table, columns="*", filters=None, limit=None)` +Выполняет SELECT запрос. + +**Параметры:** +- `table` (str): Название таблицы +- `columns` (str): Колонки для выборки +- `filters` (dict): Словарь с фильтрами `{column: value}` +- `limit` (int): Максимальное количество записей + +**Возвращает:** `List[Dict]` + +#### `insert(table, data)` +Вставляет новую запись. + +**Параметры:** +- `table` (str): Название таблицы +- `data` (dict): Данные для вставки + +**Возвращает:** `Dict` + +#### `update(table, data, filters)` +Обновляет записи. + +**Параметры:** +- `table` (str): Название таблицы +- `data` (dict): Данные для обновления +- `filters` (dict): Фильтры `{column: value}` + +**Возвращает:** `List[Dict]` + +#### `delete(table, filters)` +Удаляет записи. + +**Параметры:** +- `table` (str): Название таблицы +- `filters` (dict): Фильтры `{column: value}` + +**Возвращает:** `List[Dict]` + +#### `upload_file(bucket, file_path, file_data, content_type=None)` +Загружает файл в Storage. + +**Параметры:** +- `bucket` (str): Название bucket +- `file_path` (str): Путь к файлу в bucket +- `file_data` (bytes): Данные файла +- `content_type` (str): MIME тип файла + +**Возвращает:** `bool` + +#### `get_public_url(bucket, file_path)` +Получает публичный URL файла. + +**Параметры:** +- `bucket` (str): Название bucket +- `file_path` (str): Путь к файлу + +**Возвращает:** `str` + +#### `get_client()` +Возвращает прямой доступ к клиенту Supabase для сложных операций. + +**Возвращает:** `Client` + +## 🧪 Создание тестовой таблицы + +Для тестирования создайте таблицу в Supabase: + +```sql +CREATE TABLE users ( + id BIGSERIAL PRIMARY KEY, + name TEXT NOT NULL, + email TEXT UNIQUE NOT NULL, + age INTEGER, + created_at TIMESTAMP DEFAULT NOW() +); + +-- Тестовые данные +INSERT INTO users (name, email, age) VALUES + ('Иван Иванов', 'ivan@example.com', 30), + ('Мария Петрова', 'maria@example.com', 25), + ('Алексей Сидоров', 'alex@example.com', 35); +``` + +## 🔐 Безопасность + +- ⚠️ Никогда не коммитьте файл `.env` в git +- Используйте **anon key** для клиентских операций +- Настройте Row Level Security (RLS) в Supabase +- Для серверных операций используйте **service_role key** с осторожностью + +## 🛠️ Расширение функциональности + +Вы можете легко добавить свои методы в класс `SupabaseManager`: + +```python +class SupabaseManager: + # ... существующие методы ... + + def count_records(self, table: str) -> int: + """Подсчет количества записей в таблице""" + data = self.select(table=table) + return len(data) + + def search_by_text(self, table: str, column: str, search_term: str): + """Полнотекстовый поиск""" + return self.client.table(table)\ + .select("*")\ + .ilike(column, f"%{search_term}%")\ + .execute().data +``` + +## 🐛 Решение проблем + +### Ошибка подключения +``` +ValueError: SUPABASE_URL и SUPABASE_KEY должны быть установлены +``` +**Решение:** Проверьте файл `.env` и убедитесь, что указаны правильные значения. + +### Ошибка доступа к таблице +``` +Ошибка при выполнении SELECT: ... +``` +**Решение:** +1. Убедитесь, что таблица существует +2. Проверьте настройки RLS (Row Level Security) +3. Убедитесь, что у вашего ключа есть доступ к таблице + +## 📚 Дополнительные ресурсы + +- [Официальная документация Supabase](https://supabase.com/docs) +- [Supabase Python Client](https://supabase.com/docs/reference/python/introduction) +- [Row Level Security](https://supabase.com/docs/guides/auth/row-level-security) + +## 🤝 Вклад в проект + +Приветствуются любые предложения и улучшения! Создавайте Issues и Pull Requests. + +## 📄 Лицензия + +MIT License - используйте свободно в своих проектах. + +## ⭐ Поддержка + +Если проект был полезен, поставьте звезду на GitHub! diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..731d18d --- /dev/null +++ b/__init__.py @@ -0,0 +1,8 @@ +""" +Supabase Connector - простой Python коннектор для Supabase +""" + +from .supabase import SupabaseManager + +__version__ = "1.0.0" +__all__ = ["SupabaseManager"] diff --git a/example.py b/example.py new file mode 100644 index 0000000..c4d574c --- /dev/null +++ b/example.py @@ -0,0 +1,240 @@ +""" +Примеры использования SupabaseManager +""" + +from supabase import SupabaseManager + +def example_select(): + """Пример выборки данных из таблицы""" + print("\n=== Пример SELECT ===") + + sb = SupabaseManager() + + # Получить все записи + all_users = sb.select(table="users") + print(f"Всего пользователей: {len(all_users)}") + + # Получить с фильтром + filtered_users = sb.select( + table="users", + filters={"age": 30} + ) + print(f"Пользователей с возрастом 30: {len(filtered_users)}") + + # Получить с лимитом + limited_users = sb.select( + table="users", + limit=5 + ) + print(f"Первые 5 пользователей: {len(limited_users)}") + + # Выбрать только определенные колонки + names_only = sb.select( + table="users", + columns="id, name" + ) + for user in names_only: + print(f" - {user.get('name')} (ID: {user.get('id')})") + + +def example_insert(): + """Пример добавления новой записи""" + print("\n=== Пример INSERT ===") + + sb = SupabaseManager() + + new_user = { + "name": "Тимур Иванов", + "email": f"timur_{hash('test')}@example.com", + "age": 28 + } + + result = sb.insert(table="users", data=new_user) + + if result: + print(f"Создан пользователь: {result.get('name')} с ID {result.get('id')}") + else: + print("Ошибка при создании пользователя") + + +def example_update(): + """Пример обновления записи""" + print("\n=== Пример UPDATE ===") + + sb = SupabaseManager() + + # Сначала получим ID первого пользователя + users = sb.select(table="users", limit=1) + + if users: + user_id = users[0].get('id') + + # Обновим возраст + updated = sb.update( + table="users", + data={"age": 31}, + filters={"id": user_id} + ) + + if updated: + print(f"Обновлен пользователь ID {user_id}: {updated[0]}") + else: + print("Нет пользователей для обновления") + + +def example_delete(): + """Пример удаления записи""" + print("\n=== Пример DELETE ===") + + sb = SupabaseManager() + + # Получим последнюю запись + users = sb.select(table="users") + + if users: + last_user = users[-1] + user_id = last_user.get('id') + + print(f"Удаляем пользователя: {last_user.get('name')} (ID: {user_id})") + + deleted = sb.delete( + table="users", + filters={"id": user_id} + ) + + if deleted: + print(f"✅ Пользователь удален") + else: + print("Нет пользователей для удаления") + + +def example_complex_query(): + """Пример работы с прямым клиентом Supabase для сложных запросов""" + print("\n=== Пример сложных запросов ===") + + sb = SupabaseManager() + client = sb.get_client() + + # Использование операторов сравнения + adults = client.table("users")\ + .select("*")\ + .gte("age", 18)\ + .execute() + + print(f"Взрослых пользователей (18+): {len(adults.data)}") + + # Поиск по паттерну (LIKE) + ivan_users = client.table("users")\ + .select("*")\ + .ilike("name", "%иван%")\ + .execute() + + print(f"Пользователей с 'иван' в имени: {len(ivan_users.data)}") + + # Сортировка + sorted_users = client.table("users")\ + .select("*")\ + .order("age", desc=True)\ + .limit(3)\ + .execute() + + print("Топ-3 самых старших пользователей:") + for user in sorted_users.data: + print(f" - {user.get('name')}, {user.get('age')} лет") + + +def example_storage(): + """Пример работы с Storage (загрузка файлов)""" + print("\n=== Пример работы с Storage ===") + + sb = SupabaseManager() + + # Создадим тестовый текстовый файл + test_content = "Это тестовый файл для демонстрации работы с Supabase Storage" + file_data = test_content.encode('utf-8') + + # Загрузим файл + success = sb.upload_file( + bucket="test-bucket", + file_path="examples/test.txt", + file_data=file_data, + content_type="text/plain" + ) + + if success: + # Получим публичный URL + url = sb.get_public_url( + bucket="test-bucket", + file_path="examples/test.txt" + ) + print(f"Файл доступен по URL: {url}") + else: + print("Ошибка при загрузке файла (проверьте, существует ли bucket 'test-bucket')") + + +def example_batch_operations(): + """Пример пакетных операций""" + print("\n=== Пример пакетных операций ===") + + sb = SupabaseManager() + + # Добавим несколько пользователей + users_to_add = [ + {"name": "Алексей Петров", "email": f"alex_{i}@example.com", "age": 25 + i} + for i in range(3) + ] + + print(f"Добавляем {len(users_to_add)} пользователей...") + + for user_data in users_to_add: + result = sb.insert(table="users", data=user_data) + if result: + print(f" ✅ {result.get('name')}") + + +def example_error_handling(): + """Пример обработки ошибок""" + print("\n=== Пример обработки ошибок ===") + + try: + sb = SupabaseManager() + + # Попытка запроса к несуществующей таблице + result = sb.select(table="nonexistent_table") + + if not result: + print("⚠️ Таблица не существует или запрос вернул пустой результат") + + except Exception as e: + print(f"❌ Произошла ошибка: {e}") + + +def main(): + """Запуск всех примеров""" + print("=" * 60) + print("Примеры использования SupabaseManager") + print("=" * 60) + + try: + # Раскомментируйте нужные примеры + + example_select() + # example_insert() + # example_update() + # example_delete() + # example_complex_query() + # example_storage() + # example_batch_operations() + # example_error_handling() + + except ValueError as e: + print(f"\n❌ Ошибка инициализации: {e}") + print("\n💡 Убедитесь, что вы создали файл .env с переменными:") + print(" SUPABASE_URL=your_url") + print(" SUPABASE_KEY=your_key") + except Exception as e: + print(f"\n❌ Непредвиденная ошибка: {e}") + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d827ada --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +supabase>=2.0.0 +python-dotenv>=1.0.0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..25dc1c4 --- /dev/null +++ b/setup.py @@ -0,0 +1,39 @@ +from setuptools import setup, find_packages + +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +setup( + name="supabase-connector", + version="1.0.0", + author="Your Name", + author_email="your.email@example.com", + description="Простой Python коннектор для работы с Supabase", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/yourusername/supabase-connector", + packages=find_packages(), + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + ], + python_requires=">=3.8", + install_requires=[ + "supabase>=2.0.0", + "python-dotenv>=1.0.0", + ], + extras_require={ + "dev": [ + "pytest>=7.0.0", + "black>=22.0.0", + "flake8>=4.0.0", + ], + }, +) diff --git a/streamlit_supabase_project.tar.gz b/streamlit_supabase_project.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..9ec66db72641aaad5ae78e524194dfe0b7906e27 GIT binary patch literal 6175 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 literal 0 HcmV?d00001 diff --git a/supabase.py b/supabase.py new file mode 100644 index 0000000..0f3e06a --- /dev/null +++ b/supabase.py @@ -0,0 +1,212 @@ +import os +from typing import Optional, Dict, List, Any +from supabase import create_client, Client +from dotenv import load_dotenv + +# Загружаем переменные окружения +load_dotenv() + + +class SupabaseManager: + """ + Класс для управления подключением и операциями с Supabase + """ + + def __init__(self): + """ + Инициализация подключения к Supabase + """ + self.url: str = os.getenv("SUPABASE_URL", "") + self.key: str = os.getenv("SUPABASE_KEY", "") + self.client: Optional[Client] = None + + if not self.url or not self.key: + raise ValueError( + "SUPABASE_URL и SUPABASE_KEY должны быть установлены в переменных окружения" + ) + + self._connect() + + def _connect(self) -> None: + """ + Создает подключение к Supabase + """ + try: + self.client = create_client(self.url, self.key) + print("✅ Успешное подключение к Supabase") + except Exception as e: + print(f"❌ Ошибка подключения к Supabase: {e}") + raise + + def get_client(self) -> Client: + """ + Возвращает клиент Supabase + + Returns: + Client: Клиент Supabase + """ + if not self.client: + raise ConnectionError("Клиент Supabase не инициализирован") + return self.client + + # === Методы для работы с данными === + + def select( + self, + table: str, + columns: str = "*", + filters: Optional[Dict[str, Any]] = None, + limit: Optional[int] = None + ) -> List[Dict]: + """ + Выполняет SELECT запрос к таблице + + Args: + table: Название таблицы + columns: Колонки для выборки (по умолчанию все) + filters: Словарь с фильтрами {column: value} + limit: Лимит количества записей + + Returns: + List[Dict]: Список словарей с данными + """ + try: + query = self.client.table(table).select(columns) + + if filters: + for column, value in filters.items(): + query = query.eq(column, value) + + if limit: + query = query.limit(limit) + + response = query.execute() + return response.data + except Exception as e: + print(f"❌ Ошибка при выполнении SELECT: {e}") + return [] + + def insert(self, table: str, data: Dict[str, Any]) -> Dict: + """ + Вставляет запись в таблицу + + Args: + table: Название таблицы + data: Словарь с данными для вставки + + Returns: + Dict: Вставленная запись + """ + try: + response = self.client.table(table).insert(data).execute() + print(f"✅ Запись успешно добавлена в таблицу {table}") + return response.data[0] if response.data else {} + except Exception as e: + print(f"❌ Ошибка при вставке данных: {e}") + return {} + + def update( + self, + table: str, + data: Dict[str, Any], + filters: Dict[str, Any] + ) -> List[Dict]: + """ + Обновляет записи в таблице + + Args: + table: Название таблицы + data: Словарь с данными для обновления + filters: Словарь с фильтрами {column: value} + + Returns: + List[Dict]: Обновленные записи + """ + try: + query = self.client.table(table).update(data) + + for column, value in filters.items(): + query = query.eq(column, value) + + response = query.execute() + print(f"✅ Записи успешно обновлены в таблице {table}") + return response.data + except Exception as e: + print(f"❌ Ошибка при обновлении данных: {e}") + return [] + + def delete(self, table: str, filters: Dict[str, Any]) -> List[Dict]: + """ + Удаляет записи из таблицы + + Args: + table: Название таблицы + filters: Словарь с фильтрами {column: value} + + Returns: + List[Dict]: Удаленные записи + """ + try: + query = self.client.table(table).delete() + + for column, value in filters.items(): + query = query.eq(column, value) + + response = query.execute() + print(f"✅ Записи успешно удалены из таблицы {table}") + return response.data + except Exception as e: + print(f"❌ Ошибка при удалении данных: {e}") + return [] + + # === Методы для работы с Storage === + + def upload_file( + self, + bucket: str, + file_path: str, + file_data: bytes, + content_type: Optional[str] = None + ) -> bool: + """ + Загружает файл в Storage + + Args: + bucket: Название bucket + file_path: Путь к файлу в bucket + file_data: Данные файла в байтах + content_type: MIME тип файла + + Returns: + bool: True если загрузка успешна + """ + try: + options = {"content-type": content_type} if content_type else {} + self.client.storage.from_(bucket).upload( + file_path, + file_data, + file_options=options + ) + print(f"✅ Файл {file_path} успешно загружен в bucket {bucket}") + return True + except Exception as e: + print(f"❌ Ошибка при загрузке файла: {e}") + return False + + def get_public_url(self, bucket: str, file_path: str) -> str: + """ + Получает публичный URL файла + + Args: + bucket: Название bucket + file_path: Путь к файлу в bucket + + Returns: + str: Публичный URL + """ + try: + url = self.client.storage.from_(bucket).get_public_url(file_path) + return url + except Exception as e: + print(f"❌ Ошибка при получении URL: {e}") + return ""