commit a23c88f5b98799cf774cc9f6dc3060c559db528e Author: Тимур Абайдулин Date: Wed Nov 26 07:18:11 2025 +0300 Initial commit 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 0000000..9ec66db Binary files /dev/null and b/streamlit_supabase_project.tar.gz differ 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 ""