# 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"
}