prisma-web3-py


Nameprisma-web3-py JSON
Version 0.2.3 PyPI version JSON
download
home_pagehttps://github.com/your-org/prisma-web3
SummaryPython/SQLAlchemy async implementation of Prisma Web3 database models
upload_time2025-11-14 03:23:05
maintainerNone
docs_urlNone
authorSmallCat
requires_python>=3.8
licenseMIT
keywords database orm sqlalchemy async web3 prisma
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Prisma Web3 Python

<div align="center">

[![Python Version](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org/downloads/)
[![SQLAlchemy](https://img.shields.io/badge/SQLAlchemy-2.0-green)](https://www.sqlalchemy.org/)
[![AsyncIO](https://img.shields.io/badge/AsyncIO-✓-brightgreen)](https://docs.python.org/3/library/asyncio.html)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

**异步 Web3 数据库 ORM** - 基于 SQLAlchemy 2.0 + AsyncIO 的高性能区块链数据访问层

[特性](#-特性) • [安装](#-安装) • [快速开始](#-快速开始) • [文档](#-文档) • [示例](#-示例) • [扩展](#-扩展)

</div>

---

## 📖 目录

- [简介](#-简介)
- [特性](#-特性)
- [架构](#-架构)  
- [安装](#-安装)
- [快速开始](#-快速开始)
- [核心概念](#-核心概念)
- [详细使用](#-详细使用)
- [扩展开发](#-扩展开发)
- [API 参考](#-api-参考)
- [最佳实践](#-最佳实践)
- [常见问题](#-常见问题)

---

## 🎯 简介

**Prisma Web3 Python** 是一个专为 Web3 应用设计的异步数据库 ORM 层,提供:

- 🚀 **高性能异步操作** - 基于 AsyncIO + AsyncPG
- 🔄 **跨链支持** - 统一的数据模型处理多链资产
- 🎨 **简洁的 API** - Repository 模式,开箱即用
- 🔌 **完全可扩展** - 暴露所有底层组件,支持自定义
- 📊 **类型安全** - 完整的类型提示支持
- 🌐 **链名规范化** - 自动处理链名缩写和标准名转换

**适用场景**:
- Web3 数据分析平台
- Token 追踪和监控系统
- 链上信号聚合服务
- DeFi 数据仓库
- NFT 元数据管理

---

## ✨ 特性

### 核心功能

| 功能 | 说明 |
|------|------|
| **异步优先** | 全异步 API,支持高并发操作 |
| **跨链设计** | 单表设计存储跨链 Token,支持多链地址映射 |
| **链名智能化** | 自动规范化链名(`sol` ↔ `solana`,`bsc` ↔ `binance-smart-chain`) |
| **Repository 模式** | 预构建的数据访问层,包含常用查询方法 |
| **灵活查询** | 支持符号、名称、别名、模糊搜索 |
| **批量操作** | 高效的批量插入和更新 |
| **完整扩展性** | 暴露 Models、Repositories、Session 等所有组件 |

### 数据模型

#### Token(代币)
- 跨链 Token 信息存储
- 支持 platforms 字段存储多链地址
- 自动主链选择(按优先级)
- 社交链接、分类、别名支持

#### Signal(信号)
- Token 信号追踪
- 来源、类型、频次统计
- 时间序列分析

#### PreSignal(预信号)
- 早期信号捕获
- 多维度评分(频道呼声、多信号、KOL讨论)
- 状态管理(开放/已转换/已关闭)

#### CryptoNews(加密新闻)
- 多源新闻聚合(TechFlow、ChainCatcher 等)
- 智能实体识别(关联的代币、股票、实体)
- JSONB 高效搜索(支持复杂查询)
- 趋势分析(热门货币、热门实体)

---

## 🏗️ 架构

```
┌─────────────────────────────────────────────────────┐
│              Your Application                        │
│  (FastAPI / Flask / Django / Custom)                 │
└─────────────────────────────────────────────────────┘
                        │
                        ▼
┌─────────────────────────────────────────────────────┐
│          Prisma Web3 Python Package                  │
│                                                      │
│  ┌──────────────┐    ┌──────────────┐              │
│  │ Repositories │◄───│   Models     │              │
│  │  - Token     │    │   - Token    │              │
│  │  - Signal    │    │   - Signal   │              │
│  │  - PreSignal │    │   - PreSignal│              │
│  └──────────────┘    └──────────────┘              │
│         │                    │                       │
│         └────────┬───────────┘                       │
│                  ▼                                   │
│        ┌──────────────────┐                         │
│        │   Database       │                         │
│        │   (Session Mgmt) │                         │
│        └──────────────────┘                         │
└─────────────────────────────────────────────────────┘
                        │
                        ▼
          ┌──────────────────────────┐
          │   PostgreSQL Database    │
          │   (AsyncPG Driver)       │
          └──────────────────────────┘
```

---

## 📦 安装

### 要求

- Python 3.8+
- PostgreSQL 12+
- AsyncPG 驱动

### 使用 pip 安装

\`\`\`bash
# 基础安装
pip install prisma-web3-py

# 从源码安装(开发版)
git clone https://github.com/your-org/prisma-web3.git
cd prisma-web3/python
pip install -e .
\`\`\`

### 数据库设置

1. **创建数据库**:
\`\`\`bash
psql -U postgres
CREATE DATABASE your_database;
\`\`\`

2. **运行迁移**(使用 Prisma):
\`\`\`bash
cd ../  # 回到项目根目录
npx prisma migrate dev
\`\`\`

3. **配置环境变量**:
\`\`\`bash
# .env
DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/your_database
\`\`\`

---

## 🚀 快速开始

### 1. 初始化数据库连接

\`\`\`python
import asyncio
from prisma_web3_py import init_db, close_db, get_db

async def main():
    # 初始化数据库连接池
    await init_db()

    try:
        # 你的业务逻辑
        async with get_db() as session:
            # 使用 session 进行数据库操作
            pass
    finally:
        # 关闭连接池
        await close_db()

if __name__ == "__main__":
    asyncio.run(main())
\`\`\`

### 2. 使用 Repository 查询

\`\`\`python
from prisma_web3_py import get_db, TokenRepository

async def query_tokens():
    repo = TokenRepository()

    async with get_db() as session:
        # 获取 Token(支持链名缩写!)
        token = await repo.get_by_address(
            session,
            chain='sol',  # 自动转换为 'solana'
            token_address='oobQ3oX6ubRYMNMahG7VSCe8Z73uaQbAWFn6f22XTgo'
        )

        print(f"Token: {token.symbol} - {token.name}")
        print(f"Chain: {token.chain}")  # 输出: solana

        # 搜索 Tokens
        tokens = await repo.search_tokens(session, "BTC", limit=10)
        for t in tokens:
            print(f"- {t.symbol}: {t.name}")
\`\`\`

### 3. 插入数据

\`\`\`python
from prisma_web3_py import get_db, TokenRepository

async def insert_token():
    repo = TokenRepository()

    async with get_db() as session:
        token_data = {
            "chain": "eth",  # 使用缩写
            "token_address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984",
            "symbol": "UNI",
            "name": "Uniswap",
            "coingecko_id": "uniswap",
            "decimals": 18,
            "platforms": {
                "ethereum": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984",
                "polygon-pos": "0xb33eaad8d922b1083446dc23f610c2567fb5180f"
            }
        }

        token_id = await repo.upsert_token(session, token_data)
        await session.commit()

        print(f"Token saved with ID: {token_id}")
\`\`\`

### 4. 创建数据(重要!)

**⚠️ 注意**: 使用 Repository 的 `create()` 方法,而不是直接创建 Model 对象!

\`\`\`python
from prisma_web3_py import get_db, PreSignalRepository

async def create_pre_signal():
    repo = PreSignalRepository()

    async with get_db() as session:
        # ✅ 正确:使用 repository.create(),自动处理 chain 标准化
        pre_signal = await repo.create(
            session,
            source="jin_vip",
            chain="sol",  # 自动转换为 'solana'
            token_address="oobQ3oX6ubRYMNMahG7VSCe8Z73uaQbAWFn6f22XTgo",
            signal_type="jin_vip",
            channel_calls=5,
            multi_signals=3,
            kol_discussions=2,
            token_narrative="Some description"
        )

        if pre_signal:
            await session.commit()
            print(f"✅ Created PreSignal ID: {pre_signal.id}")
        else:
            print("❌ Failed to create PreSignal")

# ❌ 错误示例:不要直接创建 Model 对象!
async def wrong_way():
    from prisma_web3_py import PreSignal

    async with get_db() as session:
        # ❌ 这样会导致外键错误,因为 chain 没有标准化!
        pre_signal = PreSignal(
            source="jin_vip",
            chain="sol",  # 不会自动转换,导致外键约束失败
            token_address="...",
            signal_type="jin_vip"
        )
        session.add(pre_signal)  # ❌ 会报错!
\`\`\`

**为什么必须使用 Repository?**

1. **自动链名标准化**: Repository 会自动将 `'sol'` 转换为 `'solana'`
2. **外键约束**: Token 表存储的是标准名,直接使用缩写会违反外键约束
3. **数据验证**: Repository 提供额外的数据验证和错误处理
4. **一致性**: 确保所有数据以统一格式存储

### 5. 使用 CryptoNews(加密新闻)

\`\`\`python
from datetime import datetime
from prisma_web3_py import get_db, CryptoNewsRepository

async def crypto_news_example():
    repo = CryptoNewsRepository()

    async with get_db() as session:
        # 1. 导入新闻数据(从 API)
        api_data = {
            "title": "OKX 将上线 SEI (Sei),2Z (DoubleZero)现货交易",
            "category": 1,
            "source": "TechFlow",
            "content": "11 月 14 日,据官方公告...",
            "matchedCurrencies": [{"name": "SEI"}, {"name": "2Z"}],
            "entityList": ["OKX", "SEI", "2Z"],
            "createTime": "1763089364248"  # 毫秒时间戳
        }

        # 转换时间戳并创建新闻
        news_time = datetime.fromtimestamp(int(api_data["createTime"]) / 1000)
        news = await repo.create_news(
            session,
            title=api_data["title"],
            category=api_data["category"],
            source=api_data["source"],
            content=api_data["content"],
            matched_currencies=api_data.get("matchedCurrencies", []),
            entity_list=api_data.get("entityList", []),
            news_created_at=news_time
        )
        await session.commit()

        # 2. 按加密货币搜索新闻
        btc_news = await repo.search_by_currency(session, "BTC", hours=24)
        print(f"关于 BTC 的新闻: {len(btc_news)} 条")

        # 3. 按实体搜索新闻
        okx_news = await repo.search_by_entity(session, "OKX", hours=24)
        print(f"提到 OKX 的新闻: {len(okx_news)} 条")

        # 4. 获取热门货币
        trending = await repo.get_trending_currencies(session, hours=24, limit=10)
        print("热门货币:")
        for item in trending[:5]:
            print(f"  {item['currency']}: {item['mentions']} 次提及")

        # 5. 获取热门实体
        trending_entities = await repo.get_trending_entities(session, hours=24)
        print("热门实体:")
        for item in trending_entities[:5]:
            print(f"  {item['entity']}: {item['mentions']} 次提及")

        # 6. 搜索标签
        defi_news = await repo.search_by_tag(session, "defi", hours=24)
        print(f"DeFi 相关新闻: {len(defi_news)} 条")
\`\`\`

### 6. 使用 Models 直接查询

\`\`\`python
from prisma_web3_py import get_db, Token, Signal
from sqlalchemy import select, func

async def custom_query():
    async with get_db() as session:
        # 自定义复杂查询
        stmt = (
            select(Token, func.count(Signal.id).label('signal_count'))
            .join(Signal, (Token.chain == Signal.chain) &
                          (Token.token_address == Signal.token_address))
            .group_by(Token.id)
            .order_by(func.count(Signal.id).desc())
            .limit(10)
        )

        result = await session.execute(stmt)
        for token, count in result:
            print(f"{token.symbol}: {count} signals")
\`\`\`

---

## 💡 核心概念

### 1. Repository Pattern(仓储模式)

Repository 是数据访问层的抽象,隐藏了 SQL 查询细节。

所有 Repository 都继承自 `BaseRepository`,提供基础 CRUD 方法。

### 2. 链名规范化

所有 Repository 自动处理链名转换:

\`\`\`python
# 这些都可以工作
await repo.get_by_address(session, "sol", "address")   # 缩写
await repo.get_by_address(session, "eth", "address")   # 缩写
await repo.get_by_address(session, "solana", "address")  # 标准名

# Repository 会自动转换为 CoinGecko 标准名存入数据库
\`\`\`

支持的链:Ethereum (`eth`), BSC (`bsc`), Solana (`sol`), Polygon (`poly`), Arbitrum (`arb`), Base (`base`) 等 18+ 条链。

### 3. 跨链 Token 设计

Token 表采用单表设计存储跨链资产:

\`\`\`python
{
    "chain": "ethereum",        # 主链(优先级最高)
    "token_address": "0x...",   # 主链地址
    "symbol": "UNI",
    "platforms": {              # 跨链地址映射 (JSONB)
        "ethereum": "0x...",
        "polygon-pos": "0x...",
        "arbitrum-one": "0x..."
    }
}
\`\`\`

### 4. 异步 Context Manager

使用 `get_db()` 自动管理 Session 生命周期:

\`\`\`python
async with get_db() as session:
    # session 自动创建
    result = await repo.get_all(session)
    await session.commit()
    # session 自动关闭
\`\`\`

---

## 📚 详细使用

### 使用 get_db() 和 Repository 的完整指南

#### 基础模式:使用 Context Manager

\`\`\`python
from prisma_web3_py import get_db, TokenRepository

async def basic_usage():
    # 1. 创建 repository 实例
    repo = TokenRepository()

    # 2. 使用 get_db() 获取 session
    async with get_db() as session:
        # 3. 执行数据库操作
        token = await repo.get_by_address(
            session,
            chain='sol',  # 自动标准化
            token_address='xxx'
        )

        # 4. 提交事务
        await session.commit()
        # session 自动关闭
\`\`\`

#### 完整的增删改查示例

\`\`\`python
from prisma_web3_py import (
    get_db,
    TokenRepository,
    SignalRepository,
    PreSignalRepository
)

async def crud_examples():
    token_repo = TokenRepository()
    signal_repo = SignalRepository()
    pre_signal_repo = PreSignalRepository()

    async with get_db() as session:
        # ========== CREATE ==========

        # 方式1: 使用 repository.create()(推荐)
        pre_signal = await pre_signal_repo.create(
            session,
            source="source1",
            chain="sol",  # ✅ 自动转换为 'solana'
            token_address="xxx",
            signal_type="type1",
            channel_calls=5
        )

        # 方式2: 使用专用方法
        signal = await signal_repo.upsert_signal(
            session,
            chain="eth",  # ✅ 自动转换为 'ethereum'
            token_address="0x123",
            source="source1",
            signal_type="kol"
        )

        # ========== READ ==========

        # 单个查询
        token = await token_repo.get_by_address(
            session,
            chain='bsc',  # ✅ 自动转换为 'binance-smart-chain'
            token_address='0x456'
        )

        # 批量查询
        recent_tokens = await token_repo.get_recent_tokens(
            session,
            chain='sol',
            limit=100
        )

        # 搜索
        search_results = await token_repo.search_tokens(
            session,
            search_term='BTC',
            limit=20
        )

        # ========== UPDATE ==========

        # 使用 BaseRepository 的 update_by_id
        success = await token_repo.update_by_id(
            session,
            id=token.id,
            symbol='NEW_SYMBOL'
        )

        # 或者使用 upsert
        token_id = await token_repo.upsert_token(
            session,
            {
                'chain': 'eth',
                'token_address': '0x789',
                'symbol': 'UNI',
                'name': 'Uniswap'
            }
        )

        # ========== DELETE ==========

        success = await token_repo.delete_by_id(session, id=123)

        # 提交所有更改
        await session.commit()
\`\`\`

#### 处理第三方数据的正确方式

当你从外部源(API、消息队列等)接收数据时:

\`\`\`python
async def handle_external_data(data: dict):
    """
    处理外部数据的正确方式

    Args:
        data: 来自第三方的数据,例如:
        {
            "source": "jin_vip",
            "chain": "sol",  # 可能是缩写
            "token_address": "xxx",
            "signal_type": "jin_vip",
            "signals": {
                "channel_calls": 5,
                "multi_signals": 3,
                "kol_discussions": 2
            },
            "description": "Token narrative..."
        }
    """
    pre_signal_repo = PreSignalRepository()

    # 准备数据
    pre_signal_data = {
        "source": data.get("source"),
        "chain": data.get("chain"),  # 保持原样,repository 会处理
        "token_address": data.get("token_address"),
        "signal_type": data.get("signal_type"),
    }

    # 添加 signals 数据
    signals = data.get("signals", {})
    if signals:
        pre_signal_data.update({
            "channel_calls": signals.get("channel_calls", 0),
            "multi_signals": signals.get("multi_signals", 0),
            "kol_discussions": signals.get("kol_discussions", 0),
        })

    # 添加代币叙事
    if data.get("description"):
        pre_signal_data["token_narrative"] = data["description"]

    # ✅ 正确方式:使用 repository.create()
    async with get_db() as session:
        pre_signal = await pre_signal_repo.create(
            session,
            **{k: v for k, v in pre_signal_data.items() if v is not None}
        )

        if pre_signal:
            await session.commit()
            print(f"✅ Created PreSignal ID: {pre_signal.id}")
            return pre_signal
        else:
            print("❌ Failed to create PreSignal")
            return None

    # ❌ 错误方式:直接创建 Model 对象
    # from prisma_web3_py import PreSignal
    # async with get_db() as session:
    #     pre_signal = PreSignal(**pre_signal_data)  # ❌ chain 不会标准化!
    #     session.add(pre_signal)
    #     await session.commit()  # ❌ 外键约束错误!
\`\`\`

#### 批量操作

\`\`\`python
async def bulk_operations():
    repo = TokenRepository()

    async with get_db() as session:
        # 批量创建
        tokens_data = [
            {"chain": "eth", "token_address": "0x1", "symbol": "TKN1"},
            {"chain": "bsc", "token_address": "0x2", "symbol": "TKN2"},
            {"chain": "sol", "token_address": "xxx", "symbol": "TKN3"},
        ]

        for token_data in tokens_data:
            # 使用 repository.create() 确保 chain 标准化
            token = await repo.create(session, **token_data)
            if not token:
                print(f"Failed to create {token_data}")

        # 一次性提交所有更改
        await session.commit()
\`\`\`

#### 错误处理

\`\`\`python
from sqlalchemy.exc import SQLAlchemyError, IntegrityError

async def error_handling():
    repo = PreSignalRepository()

    async with get_db() as session:
        try:
            pre_signal = await repo.create(
                session,
                source="source1",
                chain="sol",
                token_address="xxx",
                signal_type="type1"
            )

            await session.commit()
            return pre_signal

        except IntegrityError as e:
            # 外键约束、唯一约束违反
            await session.rollback()
            print(f"Integrity error: {e}")
            raise

        except SQLAlchemyError as e:
            # 其他数据库错误
            await session.rollback()
            print(f"Database error: {e}")
            raise

        except Exception as e:
            # 其他错误
            await session.rollback()
            print(f"Unexpected error: {e}")
            raise
\`\`\`

### 主要操作概览

- ✅ **Token 查询、创建、更新** - 使用 `TokenRepository`
- ✅ **Signal 管理** - 使用 `SignalRepository`
- ✅ **PreSignal 处理** - 使用 `PreSignalRepository`
- ✅ **自定义查询** - 直接使用 Models + SQLAlchemy
- ✅ **批量导入** - 使用 `TokenImporter`

详细 API 文档请参考 [API 参考](#-api-参考) 部分。

---

## 🔌 扩展开发

Prisma Web3 Python 完全可扩展。详细指南请参考 [EXTENSION_GUIDE.md](EXTENSION_GUIDE.md)。

### 快速示例

#### 1. 继承 BaseRepository

\`\`\`python
from prisma_web3_py import BaseRepository, Token

class MyTokenRepository(BaseRepository[Token]):
    async def get_high_value_tokens(self, session, min_supply: float):
        # 自定义查询
        pass
\`\`\`

#### 2. 使用 Models 直接查询

\`\`\`python
from prisma_web3_py import get_db, Token
from sqlalchemy import select

async with get_db() as session:
    stmt = select(Token).where(Token.symbol == 'BTC')
    result = await session.execute(stmt)
\`\`\`

#### 3. 扩展现有 Repository

\`\`\`python
from prisma_web3_py import TokenRepository

class ExtendedTokenRepository(TokenRepository):
    async def new_feature(self, session):
        # 添加新方法
        pass
\`\`\`

---

## 📖 API 参考

### 核心组件

\`\`\`python
from prisma_web3_py import (
    # Core
    Base, get_db, init_db, close_db, AsyncSessionLocal,

    # Models
    Token, Signal, PreSignal, SignalStatus, CryptoNews,

    # Repositories
    BaseRepository, TokenRepository, SignalRepository, PreSignalRepository,
    CryptoNewsRepository,

    # Utils
    TokenImporter, ChainConfig
)
\`\`\`

### TokenRepository 主要方法

- `get_by_address(session, chain, token_address)` - 按链和地址查询
- `search_tokens(session, search_term, chain, limit)` - 搜索
- `search_by_symbol(session, symbol, exact)` - 按符号搜索
- `search_by_name(session, name, exact)` - 按名称搜索
- `search_by_alias(session, alias)` - 按别名搜索(使用 JSONB @> 操作符)
- `fuzzy_search(session, text, threshold, limit)` - 模糊搜索(支持 pg_trgm)
- `upsert_token(session, token_data)` - 插入或更新
- `get_recent_tokens(session, chain, limit)` - 最近创建
- `get_recently_updated_tokens(session, hours, chain, limit)` - 最近更新

### CryptoNewsRepository 主要方法

**创建和基础查询**:
- `create_news(session, title, category, source, content, ...)` - 创建新闻
- `get_recent_news(session, hours, source, sector, limit)` - 获取最近新闻
- `get_news_by_source(session, source, hours, limit)` - 按来源查询
- `get_news_by_sector(session, sector, hours, limit)` - 按行业查询

**JSONB 高级查询**(使用 PostgreSQL @> 操作符):
- `search_by_currency(session, currency_name, hours, limit)` - 按加密货币搜索
- `search_by_entity(session, entity_name, hours, limit)` - 按实体搜索
- `search_by_tag(session, tag, hours, limit)` - 按标签搜索

**趋势分析**(使用 jsonb_array_elements):
- `get_trending_currencies(session, hours, limit)` - 获取热门货币
- `get_trending_entities(session, hours, limit)` - 获取热门实体

**其他**:
- `search_news(session, search_term, search_in_content, limit)` - 关键词搜索
- `get_news_statistics(session, hours)` - 统计信息

完整 API 请查看源码注释。

---

## 🎯 最佳实践

### 1. 始终使用 Repository 创建数据

✅ **推荐**:
\`\`\`python
# 使用 repository.create()
repo = PreSignalRepository()
async with get_db() as session:
    result = await repo.create(session, chain='sol', ...)
    await session.commit()
\`\`\`

❌ **避免**:
\`\`\`python
# 直接创建 Model 对象
from prisma_web3_py import PreSignal
pre_signal = PreSignal(chain='sol', ...)  # 不会标准化 chain
session.add(pre_signal)
\`\`\`

### 2. 使用 Context Manager

✅ **推荐**:
\`\`\`python
async with get_db() as session:
    result = await repo.get_all(session)
    await session.commit()
    # session 自动关闭,连接归还到连接池
\`\`\`

❌ **避免**:
\`\`\`python
session = AsyncSessionLocal()  # 手动管理
result = await repo.get_all(session)
await session.close()  # 容易忘记
\`\`\`

### 3. 正确的错误处理

✅ **推荐**:
\`\`\`python
from sqlalchemy.exc import SQLAlchemyError, IntegrityError

async with get_db() as session:
    try:
        result = await repo.create(session, **data)
        await session.commit()
        return result
    except IntegrityError as e:
        # 外键、唯一约束错误
        await session.rollback()
        logger.error(f"Integrity error: {e}")
        raise
    except SQLAlchemyError as e:
        # 其他数据库错误
        await session.rollback()
        logger.error(f"Database error: {e}")
        raise
\`\`\`

### 4. 使用链名缩写

✅ **推荐**:
\`\`\`python
# 使用缩写,更简洁
await repo.get_by_address(session, 'sol', 'address')
await repo.get_by_address(session, 'eth', 'address')
await repo.get_by_address(session, 'bsc', 'address')
\`\`\`

✅ **也可以**:
\`\`\`python
# 使用标准名,更明确
await repo.get_by_address(session, 'solana', 'address')
await repo.get_by_address(session, 'ethereum', 'address')
\`\`\`

### 5. 处理外部数据

当接收外部数据(API、消息队列)时:

✅ **推荐**:
\`\`\`python
async def handle_external_data(data: dict):
    repo = PreSignalRepository()
    async with get_db() as session:
        # repository 会自动标准化 chain
        result = await repo.create(session, **data)
        await session.commit()
        return result
\`\`\`

❌ **避免**:
\`\`\`python
from prisma_web3_py import PreSignal
async with get_db() as session:
    # 外部 data['chain'] 可能是 'sol',不会被标准化
    obj = PreSignal(**data)
    session.add(obj)  # ❌ 可能导致外键错误
\`\`\`

### 6. 批量操作的事务处理

✅ **推荐**:
\`\`\`python
async with get_db() as session:
    for item in items:
        await repo.create(session, **item)
    # 一次性提交所有操作
    await session.commit()
\`\`\`

❌ **避免**:
\`\`\`python
# 每个操作都单独提交
for item in items:
    async with get_db() as session:
        await repo.create(session, **item)
        await session.commit()  # 性能低下
\`\`\`

---

## ❓ 常见问题

### Q1: 为什么会出现外键约束错误?

**A**: 最常见的原因是直接创建 Model 对象而不是使用 Repository:

\`\`\`python
# ❌ 错误:直接创建对象
from prisma_web3_py import PreSignal
pre_signal = PreSignal(chain='sol', ...)  # chain 不会标准化
session.add(pre_signal)  # ❌ 外键错误!

# ✅ 正确:使用 repository
from prisma_web3_py import PreSignalRepository
repo = PreSignalRepository()
pre_signal = await repo.create(session, chain='sol', ...)  # ✅ 自动标准化
\`\`\`

**解决方案**: 始终使用 Repository 的 `create()` 方法或专用方法来创建数据。

### Q2: 如何处理链名?

**A**: 所有 Repository 都自动规范化链名。你可以使用缩写(`sol`, `eth`, `bsc`)或标准名(`solana`, `ethereum`, `binance-smart-chain`),数据库会统一存储为 CoinGecko 标准名。

**链名映射表**:
- `sol` → `solana`
- `eth` → `ethereum`
- `bsc` → `binance-smart-chain`
- `poly` → `polygon-pos`
- `arb` → `arbitrum-one`
- `base` → `base`
- 等 18+ 条链...

### Q3: 可以直接使用 SQLAlchemy Model 吗?

**A**: **查询可以,创建不建议**。

\`\`\`python
# ✅ 查询:可以直接使用 Model
from prisma_web3_py import Token
from sqlalchemy import select

stmt = select(Token).where(Token.symbol == 'BTC')
result = await session.execute(stmt)

# ❌ 创建:不要直接创建 Model
token = Token(chain='sol', ...)  # 不会标准化
session.add(token)  # 可能导致错误

# ✅ 创建:使用 Repository
repo = TokenRepository()
token = await repo.create(session, chain='sol', ...)  # 自动标准化
\`\`\`

### Q4: 如何执行自定义查询?

**A**: 三种方式:
1. 直接使用 Models + SQLAlchemy(仅查询)
2. 继承 BaseRepository(添加新方法)
3. 扩展现有 Repository(扩展功能)

详见 [扩展开发](#-扩展开发) 或 [EXTENSION_GUIDE.md](EXTENSION_GUIDE.md)。

### Q5: 支持哪些数据库?

**A**: 目前只支持 **PostgreSQL**(使用 AsyncPG 驱动)。

### Q6: 处理外部数据时应该注意什么?

**A**: 外部数据(API、消息队列等)可能包含链名缩写,必须通过 Repository 处理:

\`\`\`python
# 外部数据
data = {"chain": "sol", "token_address": "xxx", ...}

# ✅ 正确:使用 repository.create()
repo = PreSignalRepository()
async with get_db() as session:
    result = await repo.create(session, **data)
    await session.commit()
\`\`\`

详见 [详细使用](#-详细使用) 中的"处理第三方数据的正确方式"。

---

## 📚 文档

- **扩展指南**: [EXTENSION_GUIDE.md](EXTENSION_GUIDE.md) - 如何扩展模块
- **架构文档**: [ARCHITECTURE.md](ARCHITECTURE.md) - 系统架构说明
- **导入指南**: [IMPORT_GUIDE.md](IMPORT_GUIDE.md) - Token 数据导入

---

## 🛠️ 开发工具

### 测试

\`\`\`bash
# 运行所有测试
python scripts/run_all_tests.py

# 运行特定测试
python scripts/test_token.py
python scripts/test_signal.py
python scripts/test_pre_signal.py
\`\`\`

### 数据导入

\`\`\`bash
# 导入 token 数据
python scripts/import_token_recognition_data.py
\`\`\`

### 验证

\`\`\`bash
# 验证数据一致性
python scripts/verify_consistency.py

# 测试数据库连接
python scripts/test_connection.py
\`\`\`

---

## 📝 更新日志

### v0.1.8 (最新)
- ✨ 完全暴露 Models、Repositories、Session 等组件
- ✨ 新增扩展指南(EXTENSION_GUIDE.md)
- 🐛 修复外键约束问题(链名规范化)
- 📚 新增详细 README 文档

### v0.1.6
- ✨ 新增链名自动规范化功能
- ✨ TokenRepository 新增多种搜索方法
- ♻️ 移除 TokenRecognition 模块

---

## 📄 许可证

本项目采用 [MIT](LICENSE) 许可证。

---

## 🤝 贡献指南

欢迎贡献!请遵循以下步骤:

1. Fork 本仓库
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 开启 Pull Request

---

<div align="center">

**如果这个项目对你有帮助,请给我们一个 ⭐️!**

Made with ❤️ by the Prisma Web3 Team

</div>

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/your-org/prisma-web3",
    "name": "prisma-web3-py",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": null,
    "keywords": "database, orm, sqlalchemy, async, web3, prisma",
    "author": "SmallCat",
    "author_email": "SmallCat <your-email@example.com>",
    "download_url": "https://files.pythonhosted.org/packages/7c/7f/f506b80fa1072d6cc196c4a4800fa73f195fdf33cf6427528c65e9580d42/prisma_web3_py-0.2.3.tar.gz",
    "platform": null,
    "description": "# Prisma Web3 Python\n\n<div align=\"center\">\n\n[![Python Version](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org/downloads/)\n[![SQLAlchemy](https://img.shields.io/badge/SQLAlchemy-2.0-green)](https://www.sqlalchemy.org/)\n[![AsyncIO](https://img.shields.io/badge/AsyncIO-\u2713-brightgreen)](https://docs.python.org/3/library/asyncio.html)\n[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n\n**\u5f02\u6b65 Web3 \u6570\u636e\u5e93 ORM** - \u57fa\u4e8e SQLAlchemy 2.0 + AsyncIO \u7684\u9ad8\u6027\u80fd\u533a\u5757\u94fe\u6570\u636e\u8bbf\u95ee\u5c42\n\n[\u7279\u6027](#-\u7279\u6027) \u2022 [\u5b89\u88c5](#-\u5b89\u88c5) \u2022 [\u5feb\u901f\u5f00\u59cb](#-\u5feb\u901f\u5f00\u59cb) \u2022 [\u6587\u6863](#-\u6587\u6863) \u2022 [\u793a\u4f8b](#-\u793a\u4f8b) \u2022 [\u6269\u5c55](#-\u6269\u5c55)\n\n</div>\n\n---\n\n## \ud83d\udcd6 \u76ee\u5f55\n\n- [\u7b80\u4ecb](#-\u7b80\u4ecb)\n- [\u7279\u6027](#-\u7279\u6027)\n- [\u67b6\u6784](#-\u67b6\u6784)  \n- [\u5b89\u88c5](#-\u5b89\u88c5)\n- [\u5feb\u901f\u5f00\u59cb](#-\u5feb\u901f\u5f00\u59cb)\n- [\u6838\u5fc3\u6982\u5ff5](#-\u6838\u5fc3\u6982\u5ff5)\n- [\u8be6\u7ec6\u4f7f\u7528](#-\u8be6\u7ec6\u4f7f\u7528)\n- [\u6269\u5c55\u5f00\u53d1](#-\u6269\u5c55\u5f00\u53d1)\n- [API \u53c2\u8003](#-api-\u53c2\u8003)\n- [\u6700\u4f73\u5b9e\u8df5](#-\u6700\u4f73\u5b9e\u8df5)\n- [\u5e38\u89c1\u95ee\u9898](#-\u5e38\u89c1\u95ee\u9898)\n\n---\n\n## \ud83c\udfaf \u7b80\u4ecb\n\n**Prisma Web3 Python** \u662f\u4e00\u4e2a\u4e13\u4e3a Web3 \u5e94\u7528\u8bbe\u8ba1\u7684\u5f02\u6b65\u6570\u636e\u5e93 ORM \u5c42\uff0c\u63d0\u4f9b\uff1a\n\n- \ud83d\ude80 **\u9ad8\u6027\u80fd\u5f02\u6b65\u64cd\u4f5c** - \u57fa\u4e8e AsyncIO + AsyncPG\n- \ud83d\udd04 **\u8de8\u94fe\u652f\u6301** - \u7edf\u4e00\u7684\u6570\u636e\u6a21\u578b\u5904\u7406\u591a\u94fe\u8d44\u4ea7\n- \ud83c\udfa8 **\u7b80\u6d01\u7684 API** - Repository \u6a21\u5f0f\uff0c\u5f00\u7bb1\u5373\u7528\n- \ud83d\udd0c **\u5b8c\u5168\u53ef\u6269\u5c55** - \u66b4\u9732\u6240\u6709\u5e95\u5c42\u7ec4\u4ef6\uff0c\u652f\u6301\u81ea\u5b9a\u4e49\n- \ud83d\udcca **\u7c7b\u578b\u5b89\u5168** - \u5b8c\u6574\u7684\u7c7b\u578b\u63d0\u793a\u652f\u6301\n- \ud83c\udf10 **\u94fe\u540d\u89c4\u8303\u5316** - \u81ea\u52a8\u5904\u7406\u94fe\u540d\u7f29\u5199\u548c\u6807\u51c6\u540d\u8f6c\u6362\n\n**\u9002\u7528\u573a\u666f**\uff1a\n- Web3 \u6570\u636e\u5206\u6790\u5e73\u53f0\n- Token \u8ffd\u8e2a\u548c\u76d1\u63a7\u7cfb\u7edf\n- \u94fe\u4e0a\u4fe1\u53f7\u805a\u5408\u670d\u52a1\n- DeFi \u6570\u636e\u4ed3\u5e93\n- NFT \u5143\u6570\u636e\u7ba1\u7406\n\n---\n\n## \u2728 \u7279\u6027\n\n### \u6838\u5fc3\u529f\u80fd\n\n| \u529f\u80fd | \u8bf4\u660e |\n|------|------|\n| **\u5f02\u6b65\u4f18\u5148** | \u5168\u5f02\u6b65 API\uff0c\u652f\u6301\u9ad8\u5e76\u53d1\u64cd\u4f5c |\n| **\u8de8\u94fe\u8bbe\u8ba1** | \u5355\u8868\u8bbe\u8ba1\u5b58\u50a8\u8de8\u94fe Token\uff0c\u652f\u6301\u591a\u94fe\u5730\u5740\u6620\u5c04 |\n| **\u94fe\u540d\u667a\u80fd\u5316** | \u81ea\u52a8\u89c4\u8303\u5316\u94fe\u540d\uff08`sol` \u2194 `solana`\uff0c`bsc` \u2194 `binance-smart-chain`\uff09 |\n| **Repository \u6a21\u5f0f** | \u9884\u6784\u5efa\u7684\u6570\u636e\u8bbf\u95ee\u5c42\uff0c\u5305\u542b\u5e38\u7528\u67e5\u8be2\u65b9\u6cd5 |\n| **\u7075\u6d3b\u67e5\u8be2** | \u652f\u6301\u7b26\u53f7\u3001\u540d\u79f0\u3001\u522b\u540d\u3001\u6a21\u7cca\u641c\u7d22 |\n| **\u6279\u91cf\u64cd\u4f5c** | \u9ad8\u6548\u7684\u6279\u91cf\u63d2\u5165\u548c\u66f4\u65b0 |\n| **\u5b8c\u6574\u6269\u5c55\u6027** | \u66b4\u9732 Models\u3001Repositories\u3001Session \u7b49\u6240\u6709\u7ec4\u4ef6 |\n\n### \u6570\u636e\u6a21\u578b\n\n#### Token\uff08\u4ee3\u5e01\uff09\n- \u8de8\u94fe Token \u4fe1\u606f\u5b58\u50a8\n- \u652f\u6301 platforms \u5b57\u6bb5\u5b58\u50a8\u591a\u94fe\u5730\u5740\n- \u81ea\u52a8\u4e3b\u94fe\u9009\u62e9\uff08\u6309\u4f18\u5148\u7ea7\uff09\n- \u793e\u4ea4\u94fe\u63a5\u3001\u5206\u7c7b\u3001\u522b\u540d\u652f\u6301\n\n#### Signal\uff08\u4fe1\u53f7\uff09\n- Token \u4fe1\u53f7\u8ffd\u8e2a\n- \u6765\u6e90\u3001\u7c7b\u578b\u3001\u9891\u6b21\u7edf\u8ba1\n- \u65f6\u95f4\u5e8f\u5217\u5206\u6790\n\n#### PreSignal\uff08\u9884\u4fe1\u53f7\uff09\n- \u65e9\u671f\u4fe1\u53f7\u6355\u83b7\n- \u591a\u7ef4\u5ea6\u8bc4\u5206\uff08\u9891\u9053\u547c\u58f0\u3001\u591a\u4fe1\u53f7\u3001KOL\u8ba8\u8bba\uff09\n- \u72b6\u6001\u7ba1\u7406\uff08\u5f00\u653e/\u5df2\u8f6c\u6362/\u5df2\u5173\u95ed\uff09\n\n#### CryptoNews\uff08\u52a0\u5bc6\u65b0\u95fb\uff09\n- \u591a\u6e90\u65b0\u95fb\u805a\u5408\uff08TechFlow\u3001ChainCatcher \u7b49\uff09\n- \u667a\u80fd\u5b9e\u4f53\u8bc6\u522b\uff08\u5173\u8054\u7684\u4ee3\u5e01\u3001\u80a1\u7968\u3001\u5b9e\u4f53\uff09\n- JSONB \u9ad8\u6548\u641c\u7d22\uff08\u652f\u6301\u590d\u6742\u67e5\u8be2\uff09\n- \u8d8b\u52bf\u5206\u6790\uff08\u70ed\u95e8\u8d27\u5e01\u3001\u70ed\u95e8\u5b9e\u4f53\uff09\n\n---\n\n## \ud83c\udfd7\ufe0f \u67b6\u6784\n\n```\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502              Your Application                        \u2502\n\u2502  (FastAPI / Flask / Django / Custom)                 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                        \u2502\n                        \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502          Prisma Web3 Python Package                  \u2502\n\u2502                                                      \u2502\n\u2502  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510              \u2502\n\u2502  \u2502 Repositories \u2502\u25c4\u2500\u2500\u2500\u2502   Models     \u2502              \u2502\n\u2502  \u2502  - Token     \u2502    \u2502   - Token    \u2502              \u2502\n\u2502  \u2502  - Signal    \u2502    \u2502   - Signal   \u2502              \u2502\n\u2502  \u2502  - PreSignal \u2502    \u2502   - PreSignal\u2502              \u2502\n\u2502  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518              \u2502\n\u2502         \u2502                    \u2502                       \u2502\n\u2502         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518                       \u2502\n\u2502                  \u25bc                                   \u2502\n\u2502        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                         \u2502\n\u2502        \u2502   Database       \u2502                         \u2502\n\u2502        \u2502   (Session Mgmt) \u2502                         \u2502\n\u2502        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518                         \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                        \u2502\n                        \u25bc\n          \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n          \u2502   PostgreSQL Database    \u2502\n          \u2502   (AsyncPG Driver)       \u2502\n          \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n---\n\n## \ud83d\udce6 \u5b89\u88c5\n\n### \u8981\u6c42\n\n- Python 3.8+\n- PostgreSQL 12+\n- AsyncPG \u9a71\u52a8\n\n### \u4f7f\u7528 pip \u5b89\u88c5\n\n\\`\\`\\`bash\n# \u57fa\u7840\u5b89\u88c5\npip install prisma-web3-py\n\n# \u4ece\u6e90\u7801\u5b89\u88c5\uff08\u5f00\u53d1\u7248\uff09\ngit clone https://github.com/your-org/prisma-web3.git\ncd prisma-web3/python\npip install -e .\n\\`\\`\\`\n\n### \u6570\u636e\u5e93\u8bbe\u7f6e\n\n1. **\u521b\u5efa\u6570\u636e\u5e93**\uff1a\n\\`\\`\\`bash\npsql -U postgres\nCREATE DATABASE your_database;\n\\`\\`\\`\n\n2. **\u8fd0\u884c\u8fc1\u79fb**\uff08\u4f7f\u7528 Prisma\uff09\uff1a\n\\`\\`\\`bash\ncd ../  # \u56de\u5230\u9879\u76ee\u6839\u76ee\u5f55\nnpx prisma migrate dev\n\\`\\`\\`\n\n3. **\u914d\u7f6e\u73af\u5883\u53d8\u91cf**\uff1a\n\\`\\`\\`bash\n# .env\nDATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/your_database\n\\`\\`\\`\n\n---\n\n## \ud83d\ude80 \u5feb\u901f\u5f00\u59cb\n\n### 1. \u521d\u59cb\u5316\u6570\u636e\u5e93\u8fde\u63a5\n\n\\`\\`\\`python\nimport asyncio\nfrom prisma_web3_py import init_db, close_db, get_db\n\nasync def main():\n    # \u521d\u59cb\u5316\u6570\u636e\u5e93\u8fde\u63a5\u6c60\n    await init_db()\n\n    try:\n        # \u4f60\u7684\u4e1a\u52a1\u903b\u8f91\n        async with get_db() as session:\n            # \u4f7f\u7528 session \u8fdb\u884c\u6570\u636e\u5e93\u64cd\u4f5c\n            pass\n    finally:\n        # \u5173\u95ed\u8fde\u63a5\u6c60\n        await close_db()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n\\`\\`\\`\n\n### 2. \u4f7f\u7528 Repository \u67e5\u8be2\n\n\\`\\`\\`python\nfrom prisma_web3_py import get_db, TokenRepository\n\nasync def query_tokens():\n    repo = TokenRepository()\n\n    async with get_db() as session:\n        # \u83b7\u53d6 Token\uff08\u652f\u6301\u94fe\u540d\u7f29\u5199\uff01\uff09\n        token = await repo.get_by_address(\n            session,\n            chain='sol',  # \u81ea\u52a8\u8f6c\u6362\u4e3a 'solana'\n            token_address='oobQ3oX6ubRYMNMahG7VSCe8Z73uaQbAWFn6f22XTgo'\n        )\n\n        print(f\"Token: {token.symbol} - {token.name}\")\n        print(f\"Chain: {token.chain}\")  # \u8f93\u51fa: solana\n\n        # \u641c\u7d22 Tokens\n        tokens = await repo.search_tokens(session, \"BTC\", limit=10)\n        for t in tokens:\n            print(f\"- {t.symbol}: {t.name}\")\n\\`\\`\\`\n\n### 3. \u63d2\u5165\u6570\u636e\n\n\\`\\`\\`python\nfrom prisma_web3_py import get_db, TokenRepository\n\nasync def insert_token():\n    repo = TokenRepository()\n\n    async with get_db() as session:\n        token_data = {\n            \"chain\": \"eth\",  # \u4f7f\u7528\u7f29\u5199\n            \"token_address\": \"0x1f9840a85d5af5bf1d1762f925bdaddc4201f984\",\n            \"symbol\": \"UNI\",\n            \"name\": \"Uniswap\",\n            \"coingecko_id\": \"uniswap\",\n            \"decimals\": 18,\n            \"platforms\": {\n                \"ethereum\": \"0x1f9840a85d5af5bf1d1762f925bdaddc4201f984\",\n                \"polygon-pos\": \"0xb33eaad8d922b1083446dc23f610c2567fb5180f\"\n            }\n        }\n\n        token_id = await repo.upsert_token(session, token_data)\n        await session.commit()\n\n        print(f\"Token saved with ID: {token_id}\")\n\\`\\`\\`\n\n### 4. \u521b\u5efa\u6570\u636e\uff08\u91cd\u8981\uff01\uff09\n\n**\u26a0\ufe0f \u6ce8\u610f**: \u4f7f\u7528 Repository \u7684 `create()` \u65b9\u6cd5\uff0c\u800c\u4e0d\u662f\u76f4\u63a5\u521b\u5efa Model \u5bf9\u8c61\uff01\n\n\\`\\`\\`python\nfrom prisma_web3_py import get_db, PreSignalRepository\n\nasync def create_pre_signal():\n    repo = PreSignalRepository()\n\n    async with get_db() as session:\n        # \u2705 \u6b63\u786e\uff1a\u4f7f\u7528 repository.create()\uff0c\u81ea\u52a8\u5904\u7406 chain \u6807\u51c6\u5316\n        pre_signal = await repo.create(\n            session,\n            source=\"jin_vip\",\n            chain=\"sol\",  # \u81ea\u52a8\u8f6c\u6362\u4e3a 'solana'\n            token_address=\"oobQ3oX6ubRYMNMahG7VSCe8Z73uaQbAWFn6f22XTgo\",\n            signal_type=\"jin_vip\",\n            channel_calls=5,\n            multi_signals=3,\n            kol_discussions=2,\n            token_narrative=\"Some description\"\n        )\n\n        if pre_signal:\n            await session.commit()\n            print(f\"\u2705 Created PreSignal ID: {pre_signal.id}\")\n        else:\n            print(\"\u274c Failed to create PreSignal\")\n\n# \u274c \u9519\u8bef\u793a\u4f8b\uff1a\u4e0d\u8981\u76f4\u63a5\u521b\u5efa Model \u5bf9\u8c61\uff01\nasync def wrong_way():\n    from prisma_web3_py import PreSignal\n\n    async with get_db() as session:\n        # \u274c \u8fd9\u6837\u4f1a\u5bfc\u81f4\u5916\u952e\u9519\u8bef\uff0c\u56e0\u4e3a chain \u6ca1\u6709\u6807\u51c6\u5316\uff01\n        pre_signal = PreSignal(\n            source=\"jin_vip\",\n            chain=\"sol\",  # \u4e0d\u4f1a\u81ea\u52a8\u8f6c\u6362\uff0c\u5bfc\u81f4\u5916\u952e\u7ea6\u675f\u5931\u8d25\n            token_address=\"...\",\n            signal_type=\"jin_vip\"\n        )\n        session.add(pre_signal)  # \u274c \u4f1a\u62a5\u9519\uff01\n\\`\\`\\`\n\n**\u4e3a\u4ec0\u4e48\u5fc5\u987b\u4f7f\u7528 Repository\uff1f**\n\n1. **\u81ea\u52a8\u94fe\u540d\u6807\u51c6\u5316**: Repository \u4f1a\u81ea\u52a8\u5c06 `'sol'` \u8f6c\u6362\u4e3a `'solana'`\n2. **\u5916\u952e\u7ea6\u675f**: Token \u8868\u5b58\u50a8\u7684\u662f\u6807\u51c6\u540d\uff0c\u76f4\u63a5\u4f7f\u7528\u7f29\u5199\u4f1a\u8fdd\u53cd\u5916\u952e\u7ea6\u675f\n3. **\u6570\u636e\u9a8c\u8bc1**: Repository \u63d0\u4f9b\u989d\u5916\u7684\u6570\u636e\u9a8c\u8bc1\u548c\u9519\u8bef\u5904\u7406\n4. **\u4e00\u81f4\u6027**: \u786e\u4fdd\u6240\u6709\u6570\u636e\u4ee5\u7edf\u4e00\u683c\u5f0f\u5b58\u50a8\n\n### 5. \u4f7f\u7528 CryptoNews\uff08\u52a0\u5bc6\u65b0\u95fb\uff09\n\n\\`\\`\\`python\nfrom datetime import datetime\nfrom prisma_web3_py import get_db, CryptoNewsRepository\n\nasync def crypto_news_example():\n    repo = CryptoNewsRepository()\n\n    async with get_db() as session:\n        # 1. \u5bfc\u5165\u65b0\u95fb\u6570\u636e\uff08\u4ece API\uff09\n        api_data = {\n            \"title\": \"OKX \u5c06\u4e0a\u7ebf SEI (Sei)\uff0c2Z (DoubleZero)\u73b0\u8d27\u4ea4\u6613\",\n            \"category\": 1,\n            \"source\": \"TechFlow\",\n            \"content\": \"11 \u6708 14 \u65e5\uff0c\u636e\u5b98\u65b9\u516c\u544a...\",\n            \"matchedCurrencies\": [{\"name\": \"SEI\"}, {\"name\": \"2Z\"}],\n            \"entityList\": [\"OKX\", \"SEI\", \"2Z\"],\n            \"createTime\": \"1763089364248\"  # \u6beb\u79d2\u65f6\u95f4\u6233\n        }\n\n        # \u8f6c\u6362\u65f6\u95f4\u6233\u5e76\u521b\u5efa\u65b0\u95fb\n        news_time = datetime.fromtimestamp(int(api_data[\"createTime\"]) / 1000)\n        news = await repo.create_news(\n            session,\n            title=api_data[\"title\"],\n            category=api_data[\"category\"],\n            source=api_data[\"source\"],\n            content=api_data[\"content\"],\n            matched_currencies=api_data.get(\"matchedCurrencies\", []),\n            entity_list=api_data.get(\"entityList\", []),\n            news_created_at=news_time\n        )\n        await session.commit()\n\n        # 2. \u6309\u52a0\u5bc6\u8d27\u5e01\u641c\u7d22\u65b0\u95fb\n        btc_news = await repo.search_by_currency(session, \"BTC\", hours=24)\n        print(f\"\u5173\u4e8e BTC \u7684\u65b0\u95fb: {len(btc_news)} \u6761\")\n\n        # 3. \u6309\u5b9e\u4f53\u641c\u7d22\u65b0\u95fb\n        okx_news = await repo.search_by_entity(session, \"OKX\", hours=24)\n        print(f\"\u63d0\u5230 OKX \u7684\u65b0\u95fb: {len(okx_news)} \u6761\")\n\n        # 4. \u83b7\u53d6\u70ed\u95e8\u8d27\u5e01\n        trending = await repo.get_trending_currencies(session, hours=24, limit=10)\n        print(\"\u70ed\u95e8\u8d27\u5e01:\")\n        for item in trending[:5]:\n            print(f\"  {item['currency']}: {item['mentions']} \u6b21\u63d0\u53ca\")\n\n        # 5. \u83b7\u53d6\u70ed\u95e8\u5b9e\u4f53\n        trending_entities = await repo.get_trending_entities(session, hours=24)\n        print(\"\u70ed\u95e8\u5b9e\u4f53:\")\n        for item in trending_entities[:5]:\n            print(f\"  {item['entity']}: {item['mentions']} \u6b21\u63d0\u53ca\")\n\n        # 6. \u641c\u7d22\u6807\u7b7e\n        defi_news = await repo.search_by_tag(session, \"defi\", hours=24)\n        print(f\"DeFi \u76f8\u5173\u65b0\u95fb: {len(defi_news)} \u6761\")\n\\`\\`\\`\n\n### 6. \u4f7f\u7528 Models \u76f4\u63a5\u67e5\u8be2\n\n\\`\\`\\`python\nfrom prisma_web3_py import get_db, Token, Signal\nfrom sqlalchemy import select, func\n\nasync def custom_query():\n    async with get_db() as session:\n        # \u81ea\u5b9a\u4e49\u590d\u6742\u67e5\u8be2\n        stmt = (\n            select(Token, func.count(Signal.id).label('signal_count'))\n            .join(Signal, (Token.chain == Signal.chain) &\n                          (Token.token_address == Signal.token_address))\n            .group_by(Token.id)\n            .order_by(func.count(Signal.id).desc())\n            .limit(10)\n        )\n\n        result = await session.execute(stmt)\n        for token, count in result:\n            print(f\"{token.symbol}: {count} signals\")\n\\`\\`\\`\n\n---\n\n## \ud83d\udca1 \u6838\u5fc3\u6982\u5ff5\n\n### 1. Repository Pattern\uff08\u4ed3\u50a8\u6a21\u5f0f\uff09\n\nRepository \u662f\u6570\u636e\u8bbf\u95ee\u5c42\u7684\u62bd\u8c61\uff0c\u9690\u85cf\u4e86 SQL \u67e5\u8be2\u7ec6\u8282\u3002\n\n\u6240\u6709 Repository \u90fd\u7ee7\u627f\u81ea `BaseRepository`\uff0c\u63d0\u4f9b\u57fa\u7840 CRUD \u65b9\u6cd5\u3002\n\n### 2. \u94fe\u540d\u89c4\u8303\u5316\n\n\u6240\u6709 Repository \u81ea\u52a8\u5904\u7406\u94fe\u540d\u8f6c\u6362\uff1a\n\n\\`\\`\\`python\n# \u8fd9\u4e9b\u90fd\u53ef\u4ee5\u5de5\u4f5c\nawait repo.get_by_address(session, \"sol\", \"address\")   # \u7f29\u5199\nawait repo.get_by_address(session, \"eth\", \"address\")   # \u7f29\u5199\nawait repo.get_by_address(session, \"solana\", \"address\")  # \u6807\u51c6\u540d\n\n# Repository \u4f1a\u81ea\u52a8\u8f6c\u6362\u4e3a CoinGecko \u6807\u51c6\u540d\u5b58\u5165\u6570\u636e\u5e93\n\\`\\`\\`\n\n\u652f\u6301\u7684\u94fe\uff1aEthereum (`eth`), BSC (`bsc`), Solana (`sol`), Polygon (`poly`), Arbitrum (`arb`), Base (`base`) \u7b49 18+ \u6761\u94fe\u3002\n\n### 3. \u8de8\u94fe Token \u8bbe\u8ba1\n\nToken \u8868\u91c7\u7528\u5355\u8868\u8bbe\u8ba1\u5b58\u50a8\u8de8\u94fe\u8d44\u4ea7\uff1a\n\n\\`\\`\\`python\n{\n    \"chain\": \"ethereum\",        # \u4e3b\u94fe\uff08\u4f18\u5148\u7ea7\u6700\u9ad8\uff09\n    \"token_address\": \"0x...\",   # \u4e3b\u94fe\u5730\u5740\n    \"symbol\": \"UNI\",\n    \"platforms\": {              # \u8de8\u94fe\u5730\u5740\u6620\u5c04 (JSONB)\n        \"ethereum\": \"0x...\",\n        \"polygon-pos\": \"0x...\",\n        \"arbitrum-one\": \"0x...\"\n    }\n}\n\\`\\`\\`\n\n### 4. \u5f02\u6b65 Context Manager\n\n\u4f7f\u7528 `get_db()` \u81ea\u52a8\u7ba1\u7406 Session \u751f\u547d\u5468\u671f\uff1a\n\n\\`\\`\\`python\nasync with get_db() as session:\n    # session \u81ea\u52a8\u521b\u5efa\n    result = await repo.get_all(session)\n    await session.commit()\n    # session \u81ea\u52a8\u5173\u95ed\n\\`\\`\\`\n\n---\n\n## \ud83d\udcda \u8be6\u7ec6\u4f7f\u7528\n\n### \u4f7f\u7528 get_db() \u548c Repository \u7684\u5b8c\u6574\u6307\u5357\n\n#### \u57fa\u7840\u6a21\u5f0f\uff1a\u4f7f\u7528 Context Manager\n\n\\`\\`\\`python\nfrom prisma_web3_py import get_db, TokenRepository\n\nasync def basic_usage():\n    # 1. \u521b\u5efa repository \u5b9e\u4f8b\n    repo = TokenRepository()\n\n    # 2. \u4f7f\u7528 get_db() \u83b7\u53d6 session\n    async with get_db() as session:\n        # 3. \u6267\u884c\u6570\u636e\u5e93\u64cd\u4f5c\n        token = await repo.get_by_address(\n            session,\n            chain='sol',  # \u81ea\u52a8\u6807\u51c6\u5316\n            token_address='xxx'\n        )\n\n        # 4. \u63d0\u4ea4\u4e8b\u52a1\n        await session.commit()\n        # session \u81ea\u52a8\u5173\u95ed\n\\`\\`\\`\n\n#### \u5b8c\u6574\u7684\u589e\u5220\u6539\u67e5\u793a\u4f8b\n\n\\`\\`\\`python\nfrom prisma_web3_py import (\n    get_db,\n    TokenRepository,\n    SignalRepository,\n    PreSignalRepository\n)\n\nasync def crud_examples():\n    token_repo = TokenRepository()\n    signal_repo = SignalRepository()\n    pre_signal_repo = PreSignalRepository()\n\n    async with get_db() as session:\n        # ========== CREATE ==========\n\n        # \u65b9\u5f0f1: \u4f7f\u7528 repository.create()\uff08\u63a8\u8350\uff09\n        pre_signal = await pre_signal_repo.create(\n            session,\n            source=\"source1\",\n            chain=\"sol\",  # \u2705 \u81ea\u52a8\u8f6c\u6362\u4e3a 'solana'\n            token_address=\"xxx\",\n            signal_type=\"type1\",\n            channel_calls=5\n        )\n\n        # \u65b9\u5f0f2: \u4f7f\u7528\u4e13\u7528\u65b9\u6cd5\n        signal = await signal_repo.upsert_signal(\n            session,\n            chain=\"eth\",  # \u2705 \u81ea\u52a8\u8f6c\u6362\u4e3a 'ethereum'\n            token_address=\"0x123\",\n            source=\"source1\",\n            signal_type=\"kol\"\n        )\n\n        # ========== READ ==========\n\n        # \u5355\u4e2a\u67e5\u8be2\n        token = await token_repo.get_by_address(\n            session,\n            chain='bsc',  # \u2705 \u81ea\u52a8\u8f6c\u6362\u4e3a 'binance-smart-chain'\n            token_address='0x456'\n        )\n\n        # \u6279\u91cf\u67e5\u8be2\n        recent_tokens = await token_repo.get_recent_tokens(\n            session,\n            chain='sol',\n            limit=100\n        )\n\n        # \u641c\u7d22\n        search_results = await token_repo.search_tokens(\n            session,\n            search_term='BTC',\n            limit=20\n        )\n\n        # ========== UPDATE ==========\n\n        # \u4f7f\u7528 BaseRepository \u7684 update_by_id\n        success = await token_repo.update_by_id(\n            session,\n            id=token.id,\n            symbol='NEW_SYMBOL'\n        )\n\n        # \u6216\u8005\u4f7f\u7528 upsert\n        token_id = await token_repo.upsert_token(\n            session,\n            {\n                'chain': 'eth',\n                'token_address': '0x789',\n                'symbol': 'UNI',\n                'name': 'Uniswap'\n            }\n        )\n\n        # ========== DELETE ==========\n\n        success = await token_repo.delete_by_id(session, id=123)\n\n        # \u63d0\u4ea4\u6240\u6709\u66f4\u6539\n        await session.commit()\n\\`\\`\\`\n\n#### \u5904\u7406\u7b2c\u4e09\u65b9\u6570\u636e\u7684\u6b63\u786e\u65b9\u5f0f\n\n\u5f53\u4f60\u4ece\u5916\u90e8\u6e90\uff08API\u3001\u6d88\u606f\u961f\u5217\u7b49\uff09\u63a5\u6536\u6570\u636e\u65f6\uff1a\n\n\\`\\`\\`python\nasync def handle_external_data(data: dict):\n    \"\"\"\n    \u5904\u7406\u5916\u90e8\u6570\u636e\u7684\u6b63\u786e\u65b9\u5f0f\n\n    Args:\n        data: \u6765\u81ea\u7b2c\u4e09\u65b9\u7684\u6570\u636e\uff0c\u4f8b\u5982\uff1a\n        {\n            \"source\": \"jin_vip\",\n            \"chain\": \"sol\",  # \u53ef\u80fd\u662f\u7f29\u5199\n            \"token_address\": \"xxx\",\n            \"signal_type\": \"jin_vip\",\n            \"signals\": {\n                \"channel_calls\": 5,\n                \"multi_signals\": 3,\n                \"kol_discussions\": 2\n            },\n            \"description\": \"Token narrative...\"\n        }\n    \"\"\"\n    pre_signal_repo = PreSignalRepository()\n\n    # \u51c6\u5907\u6570\u636e\n    pre_signal_data = {\n        \"source\": data.get(\"source\"),\n        \"chain\": data.get(\"chain\"),  # \u4fdd\u6301\u539f\u6837\uff0crepository \u4f1a\u5904\u7406\n        \"token_address\": data.get(\"token_address\"),\n        \"signal_type\": data.get(\"signal_type\"),\n    }\n\n    # \u6dfb\u52a0 signals \u6570\u636e\n    signals = data.get(\"signals\", {})\n    if signals:\n        pre_signal_data.update({\n            \"channel_calls\": signals.get(\"channel_calls\", 0),\n            \"multi_signals\": signals.get(\"multi_signals\", 0),\n            \"kol_discussions\": signals.get(\"kol_discussions\", 0),\n        })\n\n    # \u6dfb\u52a0\u4ee3\u5e01\u53d9\u4e8b\n    if data.get(\"description\"):\n        pre_signal_data[\"token_narrative\"] = data[\"description\"]\n\n    # \u2705 \u6b63\u786e\u65b9\u5f0f\uff1a\u4f7f\u7528 repository.create()\n    async with get_db() as session:\n        pre_signal = await pre_signal_repo.create(\n            session,\n            **{k: v for k, v in pre_signal_data.items() if v is not None}\n        )\n\n        if pre_signal:\n            await session.commit()\n            print(f\"\u2705 Created PreSignal ID: {pre_signal.id}\")\n            return pre_signal\n        else:\n            print(\"\u274c Failed to create PreSignal\")\n            return None\n\n    # \u274c \u9519\u8bef\u65b9\u5f0f\uff1a\u76f4\u63a5\u521b\u5efa Model \u5bf9\u8c61\n    # from prisma_web3_py import PreSignal\n    # async with get_db() as session:\n    #     pre_signal = PreSignal(**pre_signal_data)  # \u274c chain \u4e0d\u4f1a\u6807\u51c6\u5316\uff01\n    #     session.add(pre_signal)\n    #     await session.commit()  # \u274c \u5916\u952e\u7ea6\u675f\u9519\u8bef\uff01\n\\`\\`\\`\n\n#### \u6279\u91cf\u64cd\u4f5c\n\n\\`\\`\\`python\nasync def bulk_operations():\n    repo = TokenRepository()\n\n    async with get_db() as session:\n        # \u6279\u91cf\u521b\u5efa\n        tokens_data = [\n            {\"chain\": \"eth\", \"token_address\": \"0x1\", \"symbol\": \"TKN1\"},\n            {\"chain\": \"bsc\", \"token_address\": \"0x2\", \"symbol\": \"TKN2\"},\n            {\"chain\": \"sol\", \"token_address\": \"xxx\", \"symbol\": \"TKN3\"},\n        ]\n\n        for token_data in tokens_data:\n            # \u4f7f\u7528 repository.create() \u786e\u4fdd chain \u6807\u51c6\u5316\n            token = await repo.create(session, **token_data)\n            if not token:\n                print(f\"Failed to create {token_data}\")\n\n        # \u4e00\u6b21\u6027\u63d0\u4ea4\u6240\u6709\u66f4\u6539\n        await session.commit()\n\\`\\`\\`\n\n#### \u9519\u8bef\u5904\u7406\n\n\\`\\`\\`python\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nasync def error_handling():\n    repo = PreSignalRepository()\n\n    async with get_db() as session:\n        try:\n            pre_signal = await repo.create(\n                session,\n                source=\"source1\",\n                chain=\"sol\",\n                token_address=\"xxx\",\n                signal_type=\"type1\"\n            )\n\n            await session.commit()\n            return pre_signal\n\n        except IntegrityError as e:\n            # \u5916\u952e\u7ea6\u675f\u3001\u552f\u4e00\u7ea6\u675f\u8fdd\u53cd\n            await session.rollback()\n            print(f\"Integrity error: {e}\")\n            raise\n\n        except SQLAlchemyError as e:\n            # \u5176\u4ed6\u6570\u636e\u5e93\u9519\u8bef\n            await session.rollback()\n            print(f\"Database error: {e}\")\n            raise\n\n        except Exception as e:\n            # \u5176\u4ed6\u9519\u8bef\n            await session.rollback()\n            print(f\"Unexpected error: {e}\")\n            raise\n\\`\\`\\`\n\n### \u4e3b\u8981\u64cd\u4f5c\u6982\u89c8\n\n- \u2705 **Token \u67e5\u8be2\u3001\u521b\u5efa\u3001\u66f4\u65b0** - \u4f7f\u7528 `TokenRepository`\n- \u2705 **Signal \u7ba1\u7406** - \u4f7f\u7528 `SignalRepository`\n- \u2705 **PreSignal \u5904\u7406** - \u4f7f\u7528 `PreSignalRepository`\n- \u2705 **\u81ea\u5b9a\u4e49\u67e5\u8be2** - \u76f4\u63a5\u4f7f\u7528 Models + SQLAlchemy\n- \u2705 **\u6279\u91cf\u5bfc\u5165** - \u4f7f\u7528 `TokenImporter`\n\n\u8be6\u7ec6 API \u6587\u6863\u8bf7\u53c2\u8003 [API \u53c2\u8003](#-api-\u53c2\u8003) \u90e8\u5206\u3002\n\n---\n\n## \ud83d\udd0c \u6269\u5c55\u5f00\u53d1\n\nPrisma Web3 Python \u5b8c\u5168\u53ef\u6269\u5c55\u3002\u8be6\u7ec6\u6307\u5357\u8bf7\u53c2\u8003 [EXTENSION_GUIDE.md](EXTENSION_GUIDE.md)\u3002\n\n### \u5feb\u901f\u793a\u4f8b\n\n#### 1. \u7ee7\u627f BaseRepository\n\n\\`\\`\\`python\nfrom prisma_web3_py import BaseRepository, Token\n\nclass MyTokenRepository(BaseRepository[Token]):\n    async def get_high_value_tokens(self, session, min_supply: float):\n        # \u81ea\u5b9a\u4e49\u67e5\u8be2\n        pass\n\\`\\`\\`\n\n#### 2. \u4f7f\u7528 Models \u76f4\u63a5\u67e5\u8be2\n\n\\`\\`\\`python\nfrom prisma_web3_py import get_db, Token\nfrom sqlalchemy import select\n\nasync with get_db() as session:\n    stmt = select(Token).where(Token.symbol == 'BTC')\n    result = await session.execute(stmt)\n\\`\\`\\`\n\n#### 3. \u6269\u5c55\u73b0\u6709 Repository\n\n\\`\\`\\`python\nfrom prisma_web3_py import TokenRepository\n\nclass ExtendedTokenRepository(TokenRepository):\n    async def new_feature(self, session):\n        # \u6dfb\u52a0\u65b0\u65b9\u6cd5\n        pass\n\\`\\`\\`\n\n---\n\n## \ud83d\udcd6 API \u53c2\u8003\n\n### \u6838\u5fc3\u7ec4\u4ef6\n\n\\`\\`\\`python\nfrom prisma_web3_py import (\n    # Core\n    Base, get_db, init_db, close_db, AsyncSessionLocal,\n\n    # Models\n    Token, Signal, PreSignal, SignalStatus, CryptoNews,\n\n    # Repositories\n    BaseRepository, TokenRepository, SignalRepository, PreSignalRepository,\n    CryptoNewsRepository,\n\n    # Utils\n    TokenImporter, ChainConfig\n)\n\\`\\`\\`\n\n### TokenRepository \u4e3b\u8981\u65b9\u6cd5\n\n- `get_by_address(session, chain, token_address)` - \u6309\u94fe\u548c\u5730\u5740\u67e5\u8be2\n- `search_tokens(session, search_term, chain, limit)` - \u641c\u7d22\n- `search_by_symbol(session, symbol, exact)` - \u6309\u7b26\u53f7\u641c\u7d22\n- `search_by_name(session, name, exact)` - \u6309\u540d\u79f0\u641c\u7d22\n- `search_by_alias(session, alias)` - \u6309\u522b\u540d\u641c\u7d22\uff08\u4f7f\u7528 JSONB @> \u64cd\u4f5c\u7b26\uff09\n- `fuzzy_search(session, text, threshold, limit)` - \u6a21\u7cca\u641c\u7d22\uff08\u652f\u6301 pg_trgm\uff09\n- `upsert_token(session, token_data)` - \u63d2\u5165\u6216\u66f4\u65b0\n- `get_recent_tokens(session, chain, limit)` - \u6700\u8fd1\u521b\u5efa\n- `get_recently_updated_tokens(session, hours, chain, limit)` - \u6700\u8fd1\u66f4\u65b0\n\n### CryptoNewsRepository \u4e3b\u8981\u65b9\u6cd5\n\n**\u521b\u5efa\u548c\u57fa\u7840\u67e5\u8be2**:\n- `create_news(session, title, category, source, content, ...)` - \u521b\u5efa\u65b0\u95fb\n- `get_recent_news(session, hours, source, sector, limit)` - \u83b7\u53d6\u6700\u8fd1\u65b0\u95fb\n- `get_news_by_source(session, source, hours, limit)` - \u6309\u6765\u6e90\u67e5\u8be2\n- `get_news_by_sector(session, sector, hours, limit)` - \u6309\u884c\u4e1a\u67e5\u8be2\n\n**JSONB \u9ad8\u7ea7\u67e5\u8be2**\uff08\u4f7f\u7528 PostgreSQL @> \u64cd\u4f5c\u7b26\uff09:\n- `search_by_currency(session, currency_name, hours, limit)` - \u6309\u52a0\u5bc6\u8d27\u5e01\u641c\u7d22\n- `search_by_entity(session, entity_name, hours, limit)` - \u6309\u5b9e\u4f53\u641c\u7d22\n- `search_by_tag(session, tag, hours, limit)` - \u6309\u6807\u7b7e\u641c\u7d22\n\n**\u8d8b\u52bf\u5206\u6790**\uff08\u4f7f\u7528 jsonb_array_elements\uff09:\n- `get_trending_currencies(session, hours, limit)` - \u83b7\u53d6\u70ed\u95e8\u8d27\u5e01\n- `get_trending_entities(session, hours, limit)` - \u83b7\u53d6\u70ed\u95e8\u5b9e\u4f53\n\n**\u5176\u4ed6**:\n- `search_news(session, search_term, search_in_content, limit)` - \u5173\u952e\u8bcd\u641c\u7d22\n- `get_news_statistics(session, hours)` - \u7edf\u8ba1\u4fe1\u606f\n\n\u5b8c\u6574 API \u8bf7\u67e5\u770b\u6e90\u7801\u6ce8\u91ca\u3002\n\n---\n\n## \ud83c\udfaf \u6700\u4f73\u5b9e\u8df5\n\n### 1. \u59cb\u7ec8\u4f7f\u7528 Repository \u521b\u5efa\u6570\u636e\n\n\u2705 **\u63a8\u8350**\uff1a\n\\`\\`\\`python\n# \u4f7f\u7528 repository.create()\nrepo = PreSignalRepository()\nasync with get_db() as session:\n    result = await repo.create(session, chain='sol', ...)\n    await session.commit()\n\\`\\`\\`\n\n\u274c **\u907f\u514d**\uff1a\n\\`\\`\\`python\n# \u76f4\u63a5\u521b\u5efa Model \u5bf9\u8c61\nfrom prisma_web3_py import PreSignal\npre_signal = PreSignal(chain='sol', ...)  # \u4e0d\u4f1a\u6807\u51c6\u5316 chain\nsession.add(pre_signal)\n\\`\\`\\`\n\n### 2. \u4f7f\u7528 Context Manager\n\n\u2705 **\u63a8\u8350**\uff1a\n\\`\\`\\`python\nasync with get_db() as session:\n    result = await repo.get_all(session)\n    await session.commit()\n    # session \u81ea\u52a8\u5173\u95ed\uff0c\u8fde\u63a5\u5f52\u8fd8\u5230\u8fde\u63a5\u6c60\n\\`\\`\\`\n\n\u274c **\u907f\u514d**\uff1a\n\\`\\`\\`python\nsession = AsyncSessionLocal()  # \u624b\u52a8\u7ba1\u7406\nresult = await repo.get_all(session)\nawait session.close()  # \u5bb9\u6613\u5fd8\u8bb0\n\\`\\`\\`\n\n### 3. \u6b63\u786e\u7684\u9519\u8bef\u5904\u7406\n\n\u2705 **\u63a8\u8350**\uff1a\n\\`\\`\\`python\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nasync with get_db() as session:\n    try:\n        result = await repo.create(session, **data)\n        await session.commit()\n        return result\n    except IntegrityError as e:\n        # \u5916\u952e\u3001\u552f\u4e00\u7ea6\u675f\u9519\u8bef\n        await session.rollback()\n        logger.error(f\"Integrity error: {e}\")\n        raise\n    except SQLAlchemyError as e:\n        # \u5176\u4ed6\u6570\u636e\u5e93\u9519\u8bef\n        await session.rollback()\n        logger.error(f\"Database error: {e}\")\n        raise\n\\`\\`\\`\n\n### 4. \u4f7f\u7528\u94fe\u540d\u7f29\u5199\n\n\u2705 **\u63a8\u8350**\uff1a\n\\`\\`\\`python\n# \u4f7f\u7528\u7f29\u5199\uff0c\u66f4\u7b80\u6d01\nawait repo.get_by_address(session, 'sol', 'address')\nawait repo.get_by_address(session, 'eth', 'address')\nawait repo.get_by_address(session, 'bsc', 'address')\n\\`\\`\\`\n\n\u2705 **\u4e5f\u53ef\u4ee5**\uff1a\n\\`\\`\\`python\n# \u4f7f\u7528\u6807\u51c6\u540d\uff0c\u66f4\u660e\u786e\nawait repo.get_by_address(session, 'solana', 'address')\nawait repo.get_by_address(session, 'ethereum', 'address')\n\\`\\`\\`\n\n### 5. \u5904\u7406\u5916\u90e8\u6570\u636e\n\n\u5f53\u63a5\u6536\u5916\u90e8\u6570\u636e\uff08API\u3001\u6d88\u606f\u961f\u5217\uff09\u65f6\uff1a\n\n\u2705 **\u63a8\u8350**\uff1a\n\\`\\`\\`python\nasync def handle_external_data(data: dict):\n    repo = PreSignalRepository()\n    async with get_db() as session:\n        # repository \u4f1a\u81ea\u52a8\u6807\u51c6\u5316 chain\n        result = await repo.create(session, **data)\n        await session.commit()\n        return result\n\\`\\`\\`\n\n\u274c **\u907f\u514d**\uff1a\n\\`\\`\\`python\nfrom prisma_web3_py import PreSignal\nasync with get_db() as session:\n    # \u5916\u90e8 data['chain'] \u53ef\u80fd\u662f 'sol'\uff0c\u4e0d\u4f1a\u88ab\u6807\u51c6\u5316\n    obj = PreSignal(**data)\n    session.add(obj)  # \u274c \u53ef\u80fd\u5bfc\u81f4\u5916\u952e\u9519\u8bef\n\\`\\`\\`\n\n### 6. \u6279\u91cf\u64cd\u4f5c\u7684\u4e8b\u52a1\u5904\u7406\n\n\u2705 **\u63a8\u8350**\uff1a\n\\`\\`\\`python\nasync with get_db() as session:\n    for item in items:\n        await repo.create(session, **item)\n    # \u4e00\u6b21\u6027\u63d0\u4ea4\u6240\u6709\u64cd\u4f5c\n    await session.commit()\n\\`\\`\\`\n\n\u274c **\u907f\u514d**\uff1a\n\\`\\`\\`python\n# \u6bcf\u4e2a\u64cd\u4f5c\u90fd\u5355\u72ec\u63d0\u4ea4\nfor item in items:\n    async with get_db() as session:\n        await repo.create(session, **item)\n        await session.commit()  # \u6027\u80fd\u4f4e\u4e0b\n\\`\\`\\`\n\n---\n\n## \u2753 \u5e38\u89c1\u95ee\u9898\n\n### Q1: \u4e3a\u4ec0\u4e48\u4f1a\u51fa\u73b0\u5916\u952e\u7ea6\u675f\u9519\u8bef\uff1f\n\n**A**: \u6700\u5e38\u89c1\u7684\u539f\u56e0\u662f\u76f4\u63a5\u521b\u5efa Model \u5bf9\u8c61\u800c\u4e0d\u662f\u4f7f\u7528 Repository\uff1a\n\n\\`\\`\\`python\n# \u274c \u9519\u8bef\uff1a\u76f4\u63a5\u521b\u5efa\u5bf9\u8c61\nfrom prisma_web3_py import PreSignal\npre_signal = PreSignal(chain='sol', ...)  # chain \u4e0d\u4f1a\u6807\u51c6\u5316\nsession.add(pre_signal)  # \u274c \u5916\u952e\u9519\u8bef\uff01\n\n# \u2705 \u6b63\u786e\uff1a\u4f7f\u7528 repository\nfrom prisma_web3_py import PreSignalRepository\nrepo = PreSignalRepository()\npre_signal = await repo.create(session, chain='sol', ...)  # \u2705 \u81ea\u52a8\u6807\u51c6\u5316\n\\`\\`\\`\n\n**\u89e3\u51b3\u65b9\u6848**: \u59cb\u7ec8\u4f7f\u7528 Repository \u7684 `create()` \u65b9\u6cd5\u6216\u4e13\u7528\u65b9\u6cd5\u6765\u521b\u5efa\u6570\u636e\u3002\n\n### Q2: \u5982\u4f55\u5904\u7406\u94fe\u540d\uff1f\n\n**A**: \u6240\u6709 Repository \u90fd\u81ea\u52a8\u89c4\u8303\u5316\u94fe\u540d\u3002\u4f60\u53ef\u4ee5\u4f7f\u7528\u7f29\u5199\uff08`sol`, `eth`, `bsc`\uff09\u6216\u6807\u51c6\u540d\uff08`solana`, `ethereum`, `binance-smart-chain`\uff09\uff0c\u6570\u636e\u5e93\u4f1a\u7edf\u4e00\u5b58\u50a8\u4e3a CoinGecko \u6807\u51c6\u540d\u3002\n\n**\u94fe\u540d\u6620\u5c04\u8868**:\n- `sol` \u2192 `solana`\n- `eth` \u2192 `ethereum`\n- `bsc` \u2192 `binance-smart-chain`\n- `poly` \u2192 `polygon-pos`\n- `arb` \u2192 `arbitrum-one`\n- `base` \u2192 `base`\n- \u7b49 18+ \u6761\u94fe...\n\n### Q3: \u53ef\u4ee5\u76f4\u63a5\u4f7f\u7528 SQLAlchemy Model \u5417\uff1f\n\n**A**: **\u67e5\u8be2\u53ef\u4ee5\uff0c\u521b\u5efa\u4e0d\u5efa\u8bae**\u3002\n\n\\`\\`\\`python\n# \u2705 \u67e5\u8be2\uff1a\u53ef\u4ee5\u76f4\u63a5\u4f7f\u7528 Model\nfrom prisma_web3_py import Token\nfrom sqlalchemy import select\n\nstmt = select(Token).where(Token.symbol == 'BTC')\nresult = await session.execute(stmt)\n\n# \u274c \u521b\u5efa\uff1a\u4e0d\u8981\u76f4\u63a5\u521b\u5efa Model\ntoken = Token(chain='sol', ...)  # \u4e0d\u4f1a\u6807\u51c6\u5316\nsession.add(token)  # \u53ef\u80fd\u5bfc\u81f4\u9519\u8bef\n\n# \u2705 \u521b\u5efa\uff1a\u4f7f\u7528 Repository\nrepo = TokenRepository()\ntoken = await repo.create(session, chain='sol', ...)  # \u81ea\u52a8\u6807\u51c6\u5316\n\\`\\`\\`\n\n### Q4: \u5982\u4f55\u6267\u884c\u81ea\u5b9a\u4e49\u67e5\u8be2\uff1f\n\n**A**: \u4e09\u79cd\u65b9\u5f0f\uff1a\n1. \u76f4\u63a5\u4f7f\u7528 Models + SQLAlchemy\uff08\u4ec5\u67e5\u8be2\uff09\n2. \u7ee7\u627f BaseRepository\uff08\u6dfb\u52a0\u65b0\u65b9\u6cd5\uff09\n3. \u6269\u5c55\u73b0\u6709 Repository\uff08\u6269\u5c55\u529f\u80fd\uff09\n\n\u8be6\u89c1 [\u6269\u5c55\u5f00\u53d1](#-\u6269\u5c55\u5f00\u53d1) \u6216 [EXTENSION_GUIDE.md](EXTENSION_GUIDE.md)\u3002\n\n### Q5: \u652f\u6301\u54ea\u4e9b\u6570\u636e\u5e93\uff1f\n\n**A**: \u76ee\u524d\u53ea\u652f\u6301 **PostgreSQL**\uff08\u4f7f\u7528 AsyncPG \u9a71\u52a8\uff09\u3002\n\n### Q6: \u5904\u7406\u5916\u90e8\u6570\u636e\u65f6\u5e94\u8be5\u6ce8\u610f\u4ec0\u4e48\uff1f\n\n**A**: \u5916\u90e8\u6570\u636e\uff08API\u3001\u6d88\u606f\u961f\u5217\u7b49\uff09\u53ef\u80fd\u5305\u542b\u94fe\u540d\u7f29\u5199\uff0c\u5fc5\u987b\u901a\u8fc7 Repository \u5904\u7406\uff1a\n\n\\`\\`\\`python\n# \u5916\u90e8\u6570\u636e\ndata = {\"chain\": \"sol\", \"token_address\": \"xxx\", ...}\n\n# \u2705 \u6b63\u786e\uff1a\u4f7f\u7528 repository.create()\nrepo = PreSignalRepository()\nasync with get_db() as session:\n    result = await repo.create(session, **data)\n    await session.commit()\n\\`\\`\\`\n\n\u8be6\u89c1 [\u8be6\u7ec6\u4f7f\u7528](#-\u8be6\u7ec6\u4f7f\u7528) \u4e2d\u7684\"\u5904\u7406\u7b2c\u4e09\u65b9\u6570\u636e\u7684\u6b63\u786e\u65b9\u5f0f\"\u3002\n\n---\n\n## \ud83d\udcda \u6587\u6863\n\n- **\u6269\u5c55\u6307\u5357**: [EXTENSION_GUIDE.md](EXTENSION_GUIDE.md) - \u5982\u4f55\u6269\u5c55\u6a21\u5757\n- **\u67b6\u6784\u6587\u6863**: [ARCHITECTURE.md](ARCHITECTURE.md) - \u7cfb\u7edf\u67b6\u6784\u8bf4\u660e\n- **\u5bfc\u5165\u6307\u5357**: [IMPORT_GUIDE.md](IMPORT_GUIDE.md) - Token \u6570\u636e\u5bfc\u5165\n\n---\n\n## \ud83d\udee0\ufe0f \u5f00\u53d1\u5de5\u5177\n\n### \u6d4b\u8bd5\n\n\\`\\`\\`bash\n# \u8fd0\u884c\u6240\u6709\u6d4b\u8bd5\npython scripts/run_all_tests.py\n\n# \u8fd0\u884c\u7279\u5b9a\u6d4b\u8bd5\npython scripts/test_token.py\npython scripts/test_signal.py\npython scripts/test_pre_signal.py\n\\`\\`\\`\n\n### \u6570\u636e\u5bfc\u5165\n\n\\`\\`\\`bash\n# \u5bfc\u5165 token \u6570\u636e\npython scripts/import_token_recognition_data.py\n\\`\\`\\`\n\n### \u9a8c\u8bc1\n\n\\`\\`\\`bash\n# \u9a8c\u8bc1\u6570\u636e\u4e00\u81f4\u6027\npython scripts/verify_consistency.py\n\n# \u6d4b\u8bd5\u6570\u636e\u5e93\u8fde\u63a5\npython scripts/test_connection.py\n\\`\\`\\`\n\n---\n\n## \ud83d\udcdd \u66f4\u65b0\u65e5\u5fd7\n\n### v0.1.8 (\u6700\u65b0)\n- \u2728 \u5b8c\u5168\u66b4\u9732 Models\u3001Repositories\u3001Session \u7b49\u7ec4\u4ef6\n- \u2728 \u65b0\u589e\u6269\u5c55\u6307\u5357\uff08EXTENSION_GUIDE.md\uff09\n- \ud83d\udc1b \u4fee\u590d\u5916\u952e\u7ea6\u675f\u95ee\u9898\uff08\u94fe\u540d\u89c4\u8303\u5316\uff09\n- \ud83d\udcda \u65b0\u589e\u8be6\u7ec6 README \u6587\u6863\n\n### v0.1.6\n- \u2728 \u65b0\u589e\u94fe\u540d\u81ea\u52a8\u89c4\u8303\u5316\u529f\u80fd\n- \u2728 TokenRepository \u65b0\u589e\u591a\u79cd\u641c\u7d22\u65b9\u6cd5\n- \u267b\ufe0f \u79fb\u9664 TokenRecognition \u6a21\u5757\n\n---\n\n## \ud83d\udcc4 \u8bb8\u53ef\u8bc1\n\n\u672c\u9879\u76ee\u91c7\u7528 [MIT](LICENSE) \u8bb8\u53ef\u8bc1\u3002\n\n---\n\n## \ud83e\udd1d \u8d21\u732e\u6307\u5357\n\n\u6b22\u8fce\u8d21\u732e\uff01\u8bf7\u9075\u5faa\u4ee5\u4e0b\u6b65\u9aa4\uff1a\n\n1. Fork \u672c\u4ed3\u5e93\n2. \u521b\u5efa\u7279\u6027\u5206\u652f (`git checkout -b feature/AmazingFeature`)\n3. \u63d0\u4ea4\u66f4\u6539 (`git commit -m 'Add some AmazingFeature'`)\n4. \u63a8\u9001\u5230\u5206\u652f (`git push origin feature/AmazingFeature`)\n5. \u5f00\u542f Pull Request\n\n---\n\n<div align=\"center\">\n\n**\u5982\u679c\u8fd9\u4e2a\u9879\u76ee\u5bf9\u4f60\u6709\u5e2e\u52a9\uff0c\u8bf7\u7ed9\u6211\u4eec\u4e00\u4e2a \u2b50\ufe0f\uff01**\n\nMade with \u2764\ufe0f by the Prisma Web3 Team\n\n</div>\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Python/SQLAlchemy async implementation of Prisma Web3 database models",
    "version": "0.2.3",
    "project_urls": {
        "Documentation": "https://github.com/your-org/prisma-web3/tree/main/python",
        "Homepage": "https://github.com/your-org/prisma-web3",
        "Issues": "https://github.com/your-org/prisma-web3/issues",
        "Repository": "https://github.com/your-org/prisma-web3"
    },
    "split_keywords": [
        "database",
        " orm",
        " sqlalchemy",
        " async",
        " web3",
        " prisma"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "03fa8cc6a37102f76ee5852bf88a4d2e49b56b9bd9c46a8da56949a8ed0a2c8c",
                "md5": "8cbc2c8f4e01b31673887f2aecf93289",
                "sha256": "e28fd3a8e63adb9be117090d067180082001bdfd0c1a3f4887b588ee1f7049ea"
            },
            "downloads": -1,
            "filename": "prisma_web3_py-0.2.3-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "8cbc2c8f4e01b31673887f2aecf93289",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 50882,
            "upload_time": "2025-11-14T03:23:02",
            "upload_time_iso_8601": "2025-11-14T03:23:02.616734Z",
            "url": "https://files.pythonhosted.org/packages/03/fa/8cc6a37102f76ee5852bf88a4d2e49b56b9bd9c46a8da56949a8ed0a2c8c/prisma_web3_py-0.2.3-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "7c7ff506b80fa1072d6cc196c4a4800fa73f195fdf33cf6427528c65e9580d42",
                "md5": "e5a6776adfaaba279a1b3aaac9d69098",
                "sha256": "02d81cd712181aa1f8606b34120c0874f4e428b27eae16676ed839b30ab2e976"
            },
            "downloads": -1,
            "filename": "prisma_web3_py-0.2.3.tar.gz",
            "has_sig": false,
            "md5_digest": "e5a6776adfaaba279a1b3aaac9d69098",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 457350,
            "upload_time": "2025-11-14T03:23:05",
            "upload_time_iso_8601": "2025-11-14T03:23:05.307914Z",
            "url": "https://files.pythonhosted.org/packages/7c/7f/f506b80fa1072d6cc196c4a4800fa73f195fdf33cf6427528c65e9580d42/prisma_web3_py-0.2.3.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-11-14 03:23:05",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "your-org",
    "github_project": "prisma-web3",
    "github_not_found": true,
    "lcname": "prisma-web3-py"
}
        
Elapsed time: 2.62739s