swm-comments-attachments


Nameswm-comments-attachments JSON
Version 0.3.2 PyPI version JSON
download
home_pageNone
SummaryGeneric comments and attachments system for FastAPI applications
upload_time2025-10-22 04:48:48
maintainerNone
docs_urlNone
authorNone
requires_python>=3.11
licenseNone
keywords fastapi comments attachments azure blob-storage
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # SWM Comments & Attachments

A generic, reusable Python package for adding comments and file attachments to any entity in FastAPI applications.

## Features

- **Generic Entity Support**: Attach comments/files to any entity using `entity_type` and `entity_id`
- **Hierarchical Attachments**: Attach files directly to comments (comment → attachments relationship)
- **Multiple Storage Backends**: Azure Blob Storage, AWS S3, or local filesystem
- **FastAPI Integration**: Ready-to-use routers and dependencies
- **Multi-tenancy**: Built-in organization-level isolation
- **Type-Safe**: Full TypeScript-style typing with Pydantic v2
- **Async**: Fully async/await compatible
- **Presigned URLs**: Secure file access without exposing storage credentials
- **Document Management**: Track document types, categories, virus scanning, and checksums

## Installation

### Option 1: Install from PyPI (when published)

```bash
pip install swm-comments-attachments
```

### Option 2: Install from local repository

```bash
cd swm-comments-attachments
pip install -e .
```

### Option 3: Add as dependency in pyproject.toml

```toml
[project.dependencies]
swm-comments-attachments = { path = "../swm-comments-attachments", develop = true }
```

## Database Setup

### Option 1: Run SQL Migration (Recommended for existing projects)

```bash
# Apply the migration to your database
mysql -u username -p database_name < database/migrations/001_create_comments_attachments_tables.sql
```

### Option 2: Use Alembic (if your project uses Alembic)

```bash
# Copy the migration to your project's alembic/versions/
cp alembic/versions/2025_01_20_0001-initial_comments_attachments_tables.py your_project/alembic/versions/

# Run migration
alembic upgrade head
```

### Option 3: Auto-create with SQLAlchemy

```python
from swm_comments.models import Base
from your_app.database import engine

# Create tables
async with engine.begin() as conn:
    await conn.run_sync(Base.metadata.create_all)
```

## Quick Start

### 1. Configure Storage Backend

```python
import os
from swm_comments.storage import AzureBlobStorage, LocalStorage

def get_storage_backend():
    """Get storage based on environment."""
    azure_connection = os.getenv("AZURE_STORAGE_CONNECTION_STRING")

    if azure_connection:
        # Production/Dev: Use Azure Blob Storage
        return AzureBlobStorage(
            connection_string=azure_connection,
            container_name=os.getenv("AZURE_STORAGE_CONTAINER_NAME", "attachments")
        )
    else:
        # Local: Use filesystem
        return LocalStorage(base_path="./uploads")

storage = get_storage_backend()
```

### 2. Create Dependency Functions

```python
from fastapi import Depends
from your_app.auth import get_current_user
from your_app.database import get_db

async def get_current_user_id(current_user = Depends(get_current_user)) -> int:
    return current_user.id

async def get_current_organisation_id(current_user = Depends(get_current_user)) -> int:
    return current_user.organisation_id
```

### 3. Add Routers to FastAPI App

```python
from fastapi import FastAPI
from swm_comments.routers import create_comment_router, create_attachment_router

app = FastAPI()

# Create routers with your dependencies
comment_router = create_comment_router(
    db_session_dependency=Depends(get_db),
    user_id_dependency=Depends(get_current_user_id),
    org_id_dependency=Depends(get_current_organisation_id),
)

attachment_router = create_attachment_router(
    storage_backend=storage,
    db_session_dependency=Depends(get_db),
    user_id_dependency=Depends(get_current_user_id),
    org_id_dependency=Depends(get_current_organisation_id),
)

# Include routers
app.include_router(comment_router, prefix="/api/comments", tags=["comments"])
app.include_router(attachment_router, prefix="/api/attachments", tags=["attachments"])
```

### 4. Use the API

#### Add a comment to any entity

```bash
POST /api/comments
Content-Type: application/json

{
    "entity_type": "task",
    "entity_id": "123",
    "comment_text": "This task is blocked waiting for approval",
    "is_internal": false
}
```

#### Upload an attachment

```bash
POST /api/attachments
Content-Type: multipart/form-data

entity_type: task
entity_id: 123
file: <binary file>
description: Supporting document
```

#### Get download URL for attachment

```bash
GET /api/attachments/456/download

# Returns:
{
    "url": "https://...presigned-url...",
    "expires_at": "2025-01-20T15:30:00",
    "file_name": "document.pdf"
}
```

## Storage Backends

### Azure Blob Storage

```python
from swm_comments.storage import AzureBlobStorage

storage = AzureBlobStorage(
    connection_string="DefaultEndpointsProtocol=https;...",
    container_name="attachments"
)
```

### AWS S3

```python
from swm_comments.storage import S3Storage

storage = S3Storage(
    bucket_name="my-attachments",
    region="us-east-1",
    access_key_id="...",
    secret_access_key="..."
)
```

### Local Filesystem

```python
from swm_comments.storage import LocalStorage

storage = LocalStorage(base_path="/var/uploads")
```

## Database Models

The package provides two SQLAlchemy models:

- **Comment**: Text comments with optional internal/external visibility
  - `id` (int): Primary key
  - `entity_type` (str): Type of entity (e.g., "task", "property", "invoice")
  - `entity_id` (str): ID of the entity
  - `comment_text` (text): Comment content
  - `is_internal` (bool): Internal vs external visibility
  - `organisation_id` (int): Multi-tenancy isolation
  - `created_by_id` (int): User who created the comment
  - `created_at`, `updated_at` (datetime): Timestamps
  - `is_deleted` (bool): Soft delete flag
  - `context` (JSON): Optional additional metadata
  - `attachment_count` (computed): Number of attachments on this comment

- **Attachment**: File metadata with storage references
  - `id` (int): Primary key
  - `entity_type` (str): Type of entity
  - `entity_id` (str): ID of the entity
  - **`comment_id` (int, optional)**: Link to parent comment (hierarchical)
  - `file_name` (str): Original filename
  - `file_size` (int): File size in bytes
  - `content_type` (str): MIME type
  - `storage_backend` (str): Storage backend used
  - `storage_path` (str): Path in storage
  - `storage_container` (str): Container/bucket name
  - **`document_type_id` (int, optional)**: Document type classification
  - **`document_category` (enum)**: "normal" or "restricted"
  - **`virus_scan_status` (enum)**: "clean", "infected", "quarantined", "skipped", "pending"
  - **`checksum` (str)**: File integrity checksum (SHA-256)
  - `description` (str, optional): User-provided description
  - `organisation_id` (int): Multi-tenancy isolation
  - `uploaded_by_id` (int): User who uploaded the file
  - `uploaded_at` (datetime): Upload timestamp
  - `is_deleted` (bool): Soft delete flag
  - `context` (JSON): Optional additional metadata

### Hierarchical Attachments (Comment → Attachments)

**New in v2.0**: Attachments can now be linked directly to comments using the `comment_id` field. This creates a hierarchical relationship where files are associated with specific comments rather than just the entity.

**Benefits:**
- Better organization: Files grouped under relevant comments
- Cascade delete: Deleting a comment automatically deletes its attachments
- Visual indicators: Show attachment count on comments
- Contextual uploads: Users can attach files while discussing specific topics

**Usage:**
```python
# Upload an attachment to a comment
POST /api/attachments
Content-Type: multipart/form-data

entity_type: task
entity_id: 123
comment_id: 456  # Link to comment #456
file: <binary file>
```

**Backward Compatibility:** The `comment_id` field is optional. Attachments can still be linked directly to entities without a comment (entity-level attachments).

## API Examples

### List comments for an entity

```python
GET /api/comments?entity_type=task&entity_id=123
```

### Delete a comment

```python
DELETE /api/comments/{comment_id}
```

### Get attachment download URL

```python
GET /api/attachments/{attachment_id}/download
# Returns presigned URL valid for 1 hour
```

## Frontend Integration (React/TypeScript)

### API Client

```typescript
// api/comments.ts
import { api } from "./api";

export interface CommentCreate {
  entity_type: string;
  entity_id: string;
  comment_text: string;
  is_internal?: boolean;
  context?: Record<string, any>;
}

export interface Comment {
  id: number;
  entity_type: string;
  entity_id: string;
  comment_text: string;
  is_internal: boolean;
  created_by_id: number;
  created_at: string;
  updated_at?: string;
  organisation_id: number;
}

export const commentsApi = {
  create: async (data: CommentCreate): Promise<Comment> => {
    const response = await api.post("/api/comments", data);
    return response.data;
  },

  list: async (entityType: string, entityId: string): Promise<Comment[]> => {
    const response = await api.get("/api/comments", {
      params: { entity_type: entityType, entity_id: entityId },
    });
    return response.data;
  },

  update: async (id: number, data: Partial<CommentCreate>): Promise<Comment> => {
    const response = await api.patch(`/api/comments/${id}`, data);
    return response.data;
  },

  delete: async (id: number): Promise<void> => {
    await api.delete(`/api/comments/${id}`);
  },
};

export const attachmentsApi = {
  upload: async (
    file: File,
    entityType: string,
    entityId: string,
    description?: string
  ) => {
    const formData = new FormData();
    formData.append("file", file);
    formData.append("entity_type", entityType);
    formData.append("entity_id", entityId);
    if (description) formData.append("description", description);

    const response = await api.post("/api/attachments", formData, {
      headers: { "Content-Type": "multipart/form-data" },
    });
    return response.data;
  },

  list: async (entityType: string, entityId: string) => {
    const response = await api.get("/api/attachments", {
      params: { entity_type: entityType, entity_id: entityId },
    });
    return response.data;
  },

  getDownloadUrl: async (id: number) => {
    const response = await api.get(`/api/attachments/${id}/download`);
    return response.data;
  },

  delete: async (id: number): Promise<void> => {
    await api.delete(`/api/attachments/${id}`);
  },
};
```

### React Component Example

```tsx
// components/TaskComments.tsx
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { commentsApi } from "@/api/comments";

export const TaskComments = ({ taskId }: { taskId: number }) => {
  const queryClient = useQueryClient();

  const { data: comments, isLoading } = useQuery({
    queryKey: ["comments", "task", taskId],
    queryFn: () => commentsApi.list("task", String(taskId)),
  });

  const createMutation = useMutation({
    mutationFn: commentsApi.create,
    onSuccess: () => {
      queryClient.invalidateQueries(["comments", "task", taskId]);
    },
  });

  const deleteMutation = useMutation({
    mutationFn: commentsApi.delete,
    onSuccess: () => {
      queryClient.invalidateQueries(["comments", "task", taskId]);
    },
  });

  const handleAddComment = (text: string) => {
    createMutation.mutate({
      entity_type: "task",
      entity_id: String(taskId),
      comment_text: text,
      is_internal: false,
    });
  };

  if (isLoading) return <div>Loading comments...</div>;

  return (
    <div className="space-y-4">
      <div className="space-y-2">
        {comments?.map((comment) => (
          <div key={comment.id} className="border rounded p-3">
            <p className="text-sm">{comment.comment_text}</p>
            <div className="flex items-center justify-between mt-2">
              <span className="text-xs text-grey-600">
                {new Date(comment.created_at).toLocaleString()}
              </span>
              <button
                onClick={() => deleteMutation.mutate(comment.id)}
                className="text-xs text-red-600"
              >
                Delete
              </button>
            </div>
          </div>
        ))}
      </div>
      <CommentInput onSubmit={handleAddComment} />
    </div>
  );
};
```

## Multi-Tenancy

All operations are automatically scoped to the organization from the authenticated user:

```python
from swm_comments.routers import create_comment_router

# The router automatically filters all queries by organisation_id
# Users can only see comments from their own organization
comment_router = create_comment_router(
    db_session_dependency=Depends(get_db),
    user_id_dependency=Depends(get_current_user_id),
    org_id_dependency=Depends(get_current_organisation_id),  # Filters all queries
)
```

### Security Features

- **Automatic Organisation Filtering**: All queries filtered by `organisation_id`
- **Permission Checks**: Users can only edit/delete their own comments/attachments
- **Soft Deletes**: Data is never permanently deleted, just marked as deleted
- **Presigned URLs**: Secure file access with time-limited URLs

## Development

```bash
# Install dev dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Type checking
mypy src

# Linting
ruff check src
```

## License

MIT License - see LICENSE file for details.

## Contributing

Contributions welcome! Please see CONTRIBUTING.md for guidelines.

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "swm-comments-attachments",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.11",
    "maintainer_email": null,
    "keywords": "fastapi, comments, attachments, azure, blob-storage",
    "author": null,
    "author_email": "SkyWide <dev@skywide.ae>",
    "download_url": "https://files.pythonhosted.org/packages/5a/25/f55f3ac3f24c000dbf14eff468bc8c8394adbef721b8b64f70e00a05c52a/swm_comments_attachments-0.3.2.tar.gz",
    "platform": null,
    "description": "# SWM Comments & Attachments\n\nA generic, reusable Python package for adding comments and file attachments to any entity in FastAPI applications.\n\n## Features\n\n- **Generic Entity Support**: Attach comments/files to any entity using `entity_type` and `entity_id`\n- **Hierarchical Attachments**: Attach files directly to comments (comment \u2192 attachments relationship)\n- **Multiple Storage Backends**: Azure Blob Storage, AWS S3, or local filesystem\n- **FastAPI Integration**: Ready-to-use routers and dependencies\n- **Multi-tenancy**: Built-in organization-level isolation\n- **Type-Safe**: Full TypeScript-style typing with Pydantic v2\n- **Async**: Fully async/await compatible\n- **Presigned URLs**: Secure file access without exposing storage credentials\n- **Document Management**: Track document types, categories, virus scanning, and checksums\n\n## Installation\n\n### Option 1: Install from PyPI (when published)\n\n```bash\npip install swm-comments-attachments\n```\n\n### Option 2: Install from local repository\n\n```bash\ncd swm-comments-attachments\npip install -e .\n```\n\n### Option 3: Add as dependency in pyproject.toml\n\n```toml\n[project.dependencies]\nswm-comments-attachments = { path = \"../swm-comments-attachments\", develop = true }\n```\n\n## Database Setup\n\n### Option 1: Run SQL Migration (Recommended for existing projects)\n\n```bash\n# Apply the migration to your database\nmysql -u username -p database_name < database/migrations/001_create_comments_attachments_tables.sql\n```\n\n### Option 2: Use Alembic (if your project uses Alembic)\n\n```bash\n# Copy the migration to your project's alembic/versions/\ncp alembic/versions/2025_01_20_0001-initial_comments_attachments_tables.py your_project/alembic/versions/\n\n# Run migration\nalembic upgrade head\n```\n\n### Option 3: Auto-create with SQLAlchemy\n\n```python\nfrom swm_comments.models import Base\nfrom your_app.database import engine\n\n# Create tables\nasync with engine.begin() as conn:\n    await conn.run_sync(Base.metadata.create_all)\n```\n\n## Quick Start\n\n### 1. Configure Storage Backend\n\n```python\nimport os\nfrom swm_comments.storage import AzureBlobStorage, LocalStorage\n\ndef get_storage_backend():\n    \"\"\"Get storage based on environment.\"\"\"\n    azure_connection = os.getenv(\"AZURE_STORAGE_CONNECTION_STRING\")\n\n    if azure_connection:\n        # Production/Dev: Use Azure Blob Storage\n        return AzureBlobStorage(\n            connection_string=azure_connection,\n            container_name=os.getenv(\"AZURE_STORAGE_CONTAINER_NAME\", \"attachments\")\n        )\n    else:\n        # Local: Use filesystem\n        return LocalStorage(base_path=\"./uploads\")\n\nstorage = get_storage_backend()\n```\n\n### 2. Create Dependency Functions\n\n```python\nfrom fastapi import Depends\nfrom your_app.auth import get_current_user\nfrom your_app.database import get_db\n\nasync def get_current_user_id(current_user = Depends(get_current_user)) -> int:\n    return current_user.id\n\nasync def get_current_organisation_id(current_user = Depends(get_current_user)) -> int:\n    return current_user.organisation_id\n```\n\n### 3. Add Routers to FastAPI App\n\n```python\nfrom fastapi import FastAPI\nfrom swm_comments.routers import create_comment_router, create_attachment_router\n\napp = FastAPI()\n\n# Create routers with your dependencies\ncomment_router = create_comment_router(\n    db_session_dependency=Depends(get_db),\n    user_id_dependency=Depends(get_current_user_id),\n    org_id_dependency=Depends(get_current_organisation_id),\n)\n\nattachment_router = create_attachment_router(\n    storage_backend=storage,\n    db_session_dependency=Depends(get_db),\n    user_id_dependency=Depends(get_current_user_id),\n    org_id_dependency=Depends(get_current_organisation_id),\n)\n\n# Include routers\napp.include_router(comment_router, prefix=\"/api/comments\", tags=[\"comments\"])\napp.include_router(attachment_router, prefix=\"/api/attachments\", tags=[\"attachments\"])\n```\n\n### 4. Use the API\n\n#### Add a comment to any entity\n\n```bash\nPOST /api/comments\nContent-Type: application/json\n\n{\n    \"entity_type\": \"task\",\n    \"entity_id\": \"123\",\n    \"comment_text\": \"This task is blocked waiting for approval\",\n    \"is_internal\": false\n}\n```\n\n#### Upload an attachment\n\n```bash\nPOST /api/attachments\nContent-Type: multipart/form-data\n\nentity_type: task\nentity_id: 123\nfile: <binary file>\ndescription: Supporting document\n```\n\n#### Get download URL for attachment\n\n```bash\nGET /api/attachments/456/download\n\n# Returns:\n{\n    \"url\": \"https://...presigned-url...\",\n    \"expires_at\": \"2025-01-20T15:30:00\",\n    \"file_name\": \"document.pdf\"\n}\n```\n\n## Storage Backends\n\n### Azure Blob Storage\n\n```python\nfrom swm_comments.storage import AzureBlobStorage\n\nstorage = AzureBlobStorage(\n    connection_string=\"DefaultEndpointsProtocol=https;...\",\n    container_name=\"attachments\"\n)\n```\n\n### AWS S3\n\n```python\nfrom swm_comments.storage import S3Storage\n\nstorage = S3Storage(\n    bucket_name=\"my-attachments\",\n    region=\"us-east-1\",\n    access_key_id=\"...\",\n    secret_access_key=\"...\"\n)\n```\n\n### Local Filesystem\n\n```python\nfrom swm_comments.storage import LocalStorage\n\nstorage = LocalStorage(base_path=\"/var/uploads\")\n```\n\n## Database Models\n\nThe package provides two SQLAlchemy models:\n\n- **Comment**: Text comments with optional internal/external visibility\n  - `id` (int): Primary key\n  - `entity_type` (str): Type of entity (e.g., \"task\", \"property\", \"invoice\")\n  - `entity_id` (str): ID of the entity\n  - `comment_text` (text): Comment content\n  - `is_internal` (bool): Internal vs external visibility\n  - `organisation_id` (int): Multi-tenancy isolation\n  - `created_by_id` (int): User who created the comment\n  - `created_at`, `updated_at` (datetime): Timestamps\n  - `is_deleted` (bool): Soft delete flag\n  - `context` (JSON): Optional additional metadata\n  - `attachment_count` (computed): Number of attachments on this comment\n\n- **Attachment**: File metadata with storage references\n  - `id` (int): Primary key\n  - `entity_type` (str): Type of entity\n  - `entity_id` (str): ID of the entity\n  - **`comment_id` (int, optional)**: Link to parent comment (hierarchical)\n  - `file_name` (str): Original filename\n  - `file_size` (int): File size in bytes\n  - `content_type` (str): MIME type\n  - `storage_backend` (str): Storage backend used\n  - `storage_path` (str): Path in storage\n  - `storage_container` (str): Container/bucket name\n  - **`document_type_id` (int, optional)**: Document type classification\n  - **`document_category` (enum)**: \"normal\" or \"restricted\"\n  - **`virus_scan_status` (enum)**: \"clean\", \"infected\", \"quarantined\", \"skipped\", \"pending\"\n  - **`checksum` (str)**: File integrity checksum (SHA-256)\n  - `description` (str, optional): User-provided description\n  - `organisation_id` (int): Multi-tenancy isolation\n  - `uploaded_by_id` (int): User who uploaded the file\n  - `uploaded_at` (datetime): Upload timestamp\n  - `is_deleted` (bool): Soft delete flag\n  - `context` (JSON): Optional additional metadata\n\n### Hierarchical Attachments (Comment \u2192 Attachments)\n\n**New in v2.0**: Attachments can now be linked directly to comments using the `comment_id` field. This creates a hierarchical relationship where files are associated with specific comments rather than just the entity.\n\n**Benefits:**\n- Better organization: Files grouped under relevant comments\n- Cascade delete: Deleting a comment automatically deletes its attachments\n- Visual indicators: Show attachment count on comments\n- Contextual uploads: Users can attach files while discussing specific topics\n\n**Usage:**\n```python\n# Upload an attachment to a comment\nPOST /api/attachments\nContent-Type: multipart/form-data\n\nentity_type: task\nentity_id: 123\ncomment_id: 456  # Link to comment #456\nfile: <binary file>\n```\n\n**Backward Compatibility:** The `comment_id` field is optional. Attachments can still be linked directly to entities without a comment (entity-level attachments).\n\n## API Examples\n\n### List comments for an entity\n\n```python\nGET /api/comments?entity_type=task&entity_id=123\n```\n\n### Delete a comment\n\n```python\nDELETE /api/comments/{comment_id}\n```\n\n### Get attachment download URL\n\n```python\nGET /api/attachments/{attachment_id}/download\n# Returns presigned URL valid for 1 hour\n```\n\n## Frontend Integration (React/TypeScript)\n\n### API Client\n\n```typescript\n// api/comments.ts\nimport { api } from \"./api\";\n\nexport interface CommentCreate {\n  entity_type: string;\n  entity_id: string;\n  comment_text: string;\n  is_internal?: boolean;\n  context?: Record<string, any>;\n}\n\nexport interface Comment {\n  id: number;\n  entity_type: string;\n  entity_id: string;\n  comment_text: string;\n  is_internal: boolean;\n  created_by_id: number;\n  created_at: string;\n  updated_at?: string;\n  organisation_id: number;\n}\n\nexport const commentsApi = {\n  create: async (data: CommentCreate): Promise<Comment> => {\n    const response = await api.post(\"/api/comments\", data);\n    return response.data;\n  },\n\n  list: async (entityType: string, entityId: string): Promise<Comment[]> => {\n    const response = await api.get(\"/api/comments\", {\n      params: { entity_type: entityType, entity_id: entityId },\n    });\n    return response.data;\n  },\n\n  update: async (id: number, data: Partial<CommentCreate>): Promise<Comment> => {\n    const response = await api.patch(`/api/comments/${id}`, data);\n    return response.data;\n  },\n\n  delete: async (id: number): Promise<void> => {\n    await api.delete(`/api/comments/${id}`);\n  },\n};\n\nexport const attachmentsApi = {\n  upload: async (\n    file: File,\n    entityType: string,\n    entityId: string,\n    description?: string\n  ) => {\n    const formData = new FormData();\n    formData.append(\"file\", file);\n    formData.append(\"entity_type\", entityType);\n    formData.append(\"entity_id\", entityId);\n    if (description) formData.append(\"description\", description);\n\n    const response = await api.post(\"/api/attachments\", formData, {\n      headers: { \"Content-Type\": \"multipart/form-data\" },\n    });\n    return response.data;\n  },\n\n  list: async (entityType: string, entityId: string) => {\n    const response = await api.get(\"/api/attachments\", {\n      params: { entity_type: entityType, entity_id: entityId },\n    });\n    return response.data;\n  },\n\n  getDownloadUrl: async (id: number) => {\n    const response = await api.get(`/api/attachments/${id}/download`);\n    return response.data;\n  },\n\n  delete: async (id: number): Promise<void> => {\n    await api.delete(`/api/attachments/${id}`);\n  },\n};\n```\n\n### React Component Example\n\n```tsx\n// components/TaskComments.tsx\nimport { useQuery, useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { commentsApi } from \"@/api/comments\";\n\nexport const TaskComments = ({ taskId }: { taskId: number }) => {\n  const queryClient = useQueryClient();\n\n  const { data: comments, isLoading } = useQuery({\n    queryKey: [\"comments\", \"task\", taskId],\n    queryFn: () => commentsApi.list(\"task\", String(taskId)),\n  });\n\n  const createMutation = useMutation({\n    mutationFn: commentsApi.create,\n    onSuccess: () => {\n      queryClient.invalidateQueries([\"comments\", \"task\", taskId]);\n    },\n  });\n\n  const deleteMutation = useMutation({\n    mutationFn: commentsApi.delete,\n    onSuccess: () => {\n      queryClient.invalidateQueries([\"comments\", \"task\", taskId]);\n    },\n  });\n\n  const handleAddComment = (text: string) => {\n    createMutation.mutate({\n      entity_type: \"task\",\n      entity_id: String(taskId),\n      comment_text: text,\n      is_internal: false,\n    });\n  };\n\n  if (isLoading) return <div>Loading comments...</div>;\n\n  return (\n    <div className=\"space-y-4\">\n      <div className=\"space-y-2\">\n        {comments?.map((comment) => (\n          <div key={comment.id} className=\"border rounded p-3\">\n            <p className=\"text-sm\">{comment.comment_text}</p>\n            <div className=\"flex items-center justify-between mt-2\">\n              <span className=\"text-xs text-grey-600\">\n                {new Date(comment.created_at).toLocaleString()}\n              </span>\n              <button\n                onClick={() => deleteMutation.mutate(comment.id)}\n                className=\"text-xs text-red-600\"\n              >\n                Delete\n              </button>\n            </div>\n          </div>\n        ))}\n      </div>\n      <CommentInput onSubmit={handleAddComment} />\n    </div>\n  );\n};\n```\n\n## Multi-Tenancy\n\nAll operations are automatically scoped to the organization from the authenticated user:\n\n```python\nfrom swm_comments.routers import create_comment_router\n\n# The router automatically filters all queries by organisation_id\n# Users can only see comments from their own organization\ncomment_router = create_comment_router(\n    db_session_dependency=Depends(get_db),\n    user_id_dependency=Depends(get_current_user_id),\n    org_id_dependency=Depends(get_current_organisation_id),  # Filters all queries\n)\n```\n\n### Security Features\n\n- **Automatic Organisation Filtering**: All queries filtered by `organisation_id`\n- **Permission Checks**: Users can only edit/delete their own comments/attachments\n- **Soft Deletes**: Data is never permanently deleted, just marked as deleted\n- **Presigned URLs**: Secure file access with time-limited URLs\n\n## Development\n\n```bash\n# Install dev dependencies\npip install -e \".[dev]\"\n\n# Run tests\npytest\n\n# Type checking\nmypy src\n\n# Linting\nruff check src\n```\n\n## License\n\nMIT License - see LICENSE file for details.\n\n## Contributing\n\nContributions welcome! Please see CONTRIBUTING.md for guidelines.\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "Generic comments and attachments system for FastAPI applications",
    "version": "0.3.2",
    "project_urls": {
        "Documentation": "https://github.com/khanatifk991/swm-comments-attachments#readme",
        "Homepage": "https://github.com/khanatifk991/swm-comments-attachments",
        "Repository": "https://github.com/khanatifk991/swm-comments-attachments"
    },
    "split_keywords": [
        "fastapi",
        " comments",
        " attachments",
        " azure",
        " blob-storage"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "63aa84e5ba2ac84e4f7b39a7b024208e9fe32bbf256641ee40d9d8c39383f7d9",
                "md5": "26870a725430bbe21f9c518cd387820d",
                "sha256": "b75f7ab1cd20b682e8fb306bfe532ceb0989222b54333a7fbcc5de63ac4c9c6f"
            },
            "downloads": -1,
            "filename": "swm_comments_attachments-0.3.2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "26870a725430bbe21f9c518cd387820d",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.11",
            "size": 24085,
            "upload_time": "2025-10-22T04:48:47",
            "upload_time_iso_8601": "2025-10-22T04:48:47.735138Z",
            "url": "https://files.pythonhosted.org/packages/63/aa/84e5ba2ac84e4f7b39a7b024208e9fe32bbf256641ee40d9d8c39383f7d9/swm_comments_attachments-0.3.2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "5a25f55f3ac3f24c000dbf14eff468bc8c8394adbef721b8b64f70e00a05c52a",
                "md5": "60398adce5a4cd76c417c882af614695",
                "sha256": "0815dc1139c27744b70b28ec37b6f90068c62511f6eadefdb7c34e48a550dd57"
            },
            "downloads": -1,
            "filename": "swm_comments_attachments-0.3.2.tar.gz",
            "has_sig": false,
            "md5_digest": "60398adce5a4cd76c417c882af614695",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.11",
            "size": 33414,
            "upload_time": "2025-10-22T04:48:48",
            "upload_time_iso_8601": "2025-10-22T04:48:48.820710Z",
            "url": "https://files.pythonhosted.org/packages/5a/25/f55f3ac3f24c000dbf14eff468bc8c8394adbef721b8b64f70e00a05c52a/swm_comments_attachments-0.3.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-10-22 04:48:48",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "khanatifk991",
    "github_project": "swm-comments-attachments#readme",
    "github_not_found": true,
    "lcname": "swm-comments-attachments"
}
        
Elapsed time: 2.24150s