fastapi-auth-starter 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. fastapi_auth_starter/__init__.py +7 -0
  2. fastapi_auth_starter/cli.py +326 -0
  3. fastapi_auth_starter-0.1.3.data/data/README.md +247 -0
  4. fastapi_auth_starter-0.1.3.data/data/alembic/README +1 -0
  5. fastapi_auth_starter-0.1.3.data/data/alembic/env.py +100 -0
  6. fastapi_auth_starter-0.1.3.data/data/alembic/script.py.mako +28 -0
  7. fastapi_auth_starter-0.1.3.data/data/alembic/versions/279c472f4fd8_add_user_table.py +42 -0
  8. fastapi_auth_starter-0.1.3.data/data/alembic/versions/5f062b3648fa_change_user_id_from_uuid_to_string_for_.py +38 -0
  9. fastapi_auth_starter-0.1.3.data/data/alembic/versions/8d275132562b_create_tasks_table.py +44 -0
  10. fastapi_auth_starter-0.1.3.data/data/alembic.ini +150 -0
  11. fastapi_auth_starter-0.1.3.data/data/app/__init__.py +5 -0
  12. fastapi_auth_starter-0.1.3.data/data/app/api/__init__.py +4 -0
  13. fastapi_auth_starter-0.1.3.data/data/app/api/v1/__init__.py +4 -0
  14. fastapi_auth_starter-0.1.3.data/data/app/api/v1/api.py +21 -0
  15. fastapi_auth_starter-0.1.3.data/data/app/api/v1/routes/__init__.py +4 -0
  16. fastapi_auth_starter-0.1.3.data/data/app/api/v1/routes/auth.py +513 -0
  17. fastapi_auth_starter-0.1.3.data/data/app/api/v1/routes/health.py +50 -0
  18. fastapi_auth_starter-0.1.3.data/data/app/api/v1/routes/task.py +182 -0
  19. fastapi_auth_starter-0.1.3.data/data/app/api/v1/routes/user.py +144 -0
  20. fastapi_auth_starter-0.1.3.data/data/app/api/v1/schemas/__init__.py +8 -0
  21. fastapi_auth_starter-0.1.3.data/data/app/api/v1/schemas/auth.py +198 -0
  22. fastapi_auth_starter-0.1.3.data/data/app/api/v1/schemas/task.py +61 -0
  23. fastapi_auth_starter-0.1.3.data/data/app/api/v1/schemas/user.py +96 -0
  24. fastapi_auth_starter-0.1.3.data/data/app/core/__init__.py +4 -0
  25. fastapi_auth_starter-0.1.3.data/data/app/core/config.py +107 -0
  26. fastapi_auth_starter-0.1.3.data/data/app/core/database.py +106 -0
  27. fastapi_auth_starter-0.1.3.data/data/app/core/dependencies.py +148 -0
  28. fastapi_auth_starter-0.1.3.data/data/app/core/exceptions.py +7 -0
  29. fastapi_auth_starter-0.1.3.data/data/app/db/__init__.py +4 -0
  30. fastapi_auth_starter-0.1.3.data/data/app/main.py +91 -0
  31. fastapi_auth_starter-0.1.3.data/data/app/models/__init__.py +14 -0
  32. fastapi_auth_starter-0.1.3.data/data/app/models/task.py +56 -0
  33. fastapi_auth_starter-0.1.3.data/data/app/models/user.py +45 -0
  34. fastapi_auth_starter-0.1.3.data/data/app/services/__init__.py +8 -0
  35. fastapi_auth_starter-0.1.3.data/data/app/services/auth.py +405 -0
  36. fastapi_auth_starter-0.1.3.data/data/app/services/task.py +165 -0
  37. fastapi_auth_starter-0.1.3.data/data/app/services/user.py +108 -0
  38. fastapi_auth_starter-0.1.3.data/data/pyproject.toml +77 -0
  39. fastapi_auth_starter-0.1.3.data/data/runtime.txt +2 -0
  40. fastapi_auth_starter-0.1.3.data/data/vercel.json +19 -0
  41. fastapi_auth_starter-0.1.3.dist-info/METADATA +283 -0
  42. fastapi_auth_starter-0.1.3.dist-info/RECORD +44 -0
  43. fastapi_auth_starter-0.1.3.dist-info/WHEEL +4 -0
  44. fastapi_auth_starter-0.1.3.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,182 @@
1
+ """
2
+ Task API routes
3
+ CRUD endpoints for task management
4
+ Reference: https://fastapi.tiangolo.com/tutorial/sql-databases/
5
+ """
6
+ from typing import List, Optional
7
+ from fastapi import APIRouter, Depends, HTTPException, status, Query
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+
10
+ from app.core.database import get_db
11
+ from app.api.v1.schemas.task import TaskCreate, TaskUpdate, TaskResponse
12
+ from app.services.task import TaskService
13
+
14
+
15
+ # Create router for task endpoints
16
+ router = APIRouter(
17
+ prefix="/tasks",
18
+ tags=["tasks"], # Groups endpoints in API documentation
19
+ responses={
20
+ 404: {"description": "Task not found"},
21
+ 500: {"description": "Internal server error"}
22
+ }
23
+ )
24
+
25
+
26
+ @router.get(
27
+ "",
28
+ response_model=List[TaskResponse],
29
+ summary="List tasks",
30
+ description="Retrieve a list of tasks with optional filtering and pagination",
31
+ status_code=status.HTTP_200_OK
32
+ )
33
+ async def get_tasks(
34
+ skip: int = Query(0, ge=0, description="Number of tasks to skip"),
35
+ limit: int = Query(100, ge=1, le=1000, description="Maximum number of tasks to return"),
36
+ completed: Optional[bool] = Query(None, description="Filter by completion status"),
37
+ db: AsyncSession = Depends(get_db)
38
+ ) -> List[TaskResponse]:
39
+ """
40
+ Get a list of tasks
41
+
42
+ Supports:
43
+ - Pagination via skip and limit parameters
44
+ - Filtering by completion status
45
+
46
+ Returns:
47
+ List of TaskResponse objects
48
+ """
49
+ tasks = await TaskService.get_tasks(db, skip=skip, limit=limit, completed=completed)
50
+ return tasks
51
+
52
+
53
+ @router.get(
54
+ "/{task_id}",
55
+ response_model=TaskResponse,
56
+ summary="Get task by ID",
57
+ description="Retrieve a single task by its ID",
58
+ status_code=status.HTTP_200_OK,
59
+ responses={
60
+ 404: {"description": "Task not found"}
61
+ }
62
+ )
63
+ async def get_task(
64
+ task_id: int,
65
+ db: AsyncSession = Depends(get_db)
66
+ ) -> TaskResponse:
67
+ """
68
+ Get a single task by ID
69
+
70
+ Args:
71
+ task_id: ID of the task to retrieve
72
+
73
+ Returns:
74
+ TaskResponse object
75
+
76
+ Raises:
77
+ HTTPException: If task is not found
78
+ """
79
+ task = await TaskService.get_task(db, task_id)
80
+ if not task:
81
+ raise HTTPException(
82
+ status_code=status.HTTP_404_NOT_FOUND,
83
+ detail=f"Task with ID {task_id} not found"
84
+ )
85
+ return task
86
+
87
+
88
+ @router.post(
89
+ "",
90
+ response_model=TaskResponse,
91
+ summary="Create task",
92
+ description="Create a new task",
93
+ status_code=status.HTTP_201_CREATED
94
+ )
95
+ async def create_task(
96
+ task_data: TaskCreate,
97
+ db: AsyncSession = Depends(get_db)
98
+ ) -> TaskResponse:
99
+ """
100
+ Create a new task
101
+
102
+ Args:
103
+ task_data: Task creation data
104
+
105
+ Returns:
106
+ Created TaskResponse object
107
+ """
108
+ task = await TaskService.create_task(db, task_data)
109
+ return task
110
+
111
+
112
+ @router.put(
113
+ "/{task_id}",
114
+ response_model=TaskResponse,
115
+ summary="Update task",
116
+ description="Update an existing task (all fields required)",
117
+ status_code=status.HTTP_200_OK,
118
+ responses={
119
+ 404: {"description": "Task not found"}
120
+ }
121
+ )
122
+ async def update_task(
123
+ task_id: int,
124
+ task_data: TaskUpdate,
125
+ db: AsyncSession = Depends(get_db)
126
+ ) -> TaskResponse:
127
+ """
128
+ Update an existing task
129
+
130
+ All fields in TaskUpdate are optional, allowing partial updates.
131
+ Only provided fields will be updated.
132
+
133
+ Args:
134
+ task_id: ID of the task to update
135
+ task_data: Task update data
136
+
137
+ Returns:
138
+ Updated TaskResponse object
139
+
140
+ Raises:
141
+ HTTPException: If task is not found
142
+ """
143
+ task = await TaskService.update_task(db, task_id, task_data)
144
+ if not task:
145
+ raise HTTPException(
146
+ status_code=status.HTTP_404_NOT_FOUND,
147
+ detail=f"Task with ID {task_id} not found"
148
+ )
149
+ return task
150
+
151
+
152
+ @router.delete(
153
+ "/{task_id}",
154
+ summary="Delete task",
155
+ description="Delete a task by ID",
156
+ status_code=status.HTTP_204_NO_CONTENT,
157
+ responses={
158
+ 404: {"description": "Task not found"},
159
+ 204: {"description": "Task deleted successfully"}
160
+ }
161
+ )
162
+ async def delete_task(
163
+ task_id: int,
164
+ db: AsyncSession = Depends(get_db)
165
+ ):
166
+ """
167
+ Delete a task
168
+
169
+ Args:
170
+ task_id: ID of the task to delete
171
+
172
+ Raises:
173
+ HTTPException: If task is not found
174
+ """
175
+ deleted = await TaskService.delete_task(db, task_id)
176
+ if not deleted:
177
+ raise HTTPException(
178
+ status_code=status.HTTP_404_NOT_FOUND,
179
+ detail=f"Task with ID {task_id} not found"
180
+ )
181
+ return None # 204 No Content
182
+
@@ -0,0 +1,144 @@
1
+ from typing import List
2
+ from fastapi import APIRouter, Depends, HTTPException, Query, status
3
+ from workos.exceptions import BadRequestException
4
+ from app.api.v1.schemas.auth import WorkOSUserResponse
5
+ from app.api.v1.schemas.user import UserResponse, UserUpdate
6
+ from sqlalchemy.ext.asyncio import AsyncSession
7
+
8
+ from app.core.database import get_db
9
+ # from app.core.exceptions import InvalidPasswordException
10
+ from app.core.dependencies import get_current_user
11
+ from app.services.user import UserService
12
+ import logging
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ router = APIRouter(
17
+ prefix="/users",
18
+ tags=["users"],
19
+ )
20
+
21
+ @router.get("", response_model=List[UserResponse])
22
+ async def get_users(
23
+ skip: int = Query(0, ge=0, description="Number of users to skip"),
24
+ limit: int = Query(100, ge=1, le=1000, description="Maximum number of users to return"),
25
+ current_user: WorkOSUserResponse = Depends(get_current_user),
26
+ db: AsyncSession = Depends(get_db)
27
+ ) -> List[UserResponse]:
28
+ """
29
+ Get a list of users
30
+
31
+ Args:
32
+ skip: Number of users to skip (for pagination)
33
+ limit: Maximum number of users to return (for pagination)
34
+ db: Database session
35
+
36
+ Returns:
37
+ List of UserResponse objects
38
+ """
39
+ user_service = UserService()
40
+ users = await user_service.get_users(db, skip=skip, limit=limit)
41
+ return users
42
+
43
+
44
+ @router.get("/{user_id}", response_model=UserResponse)
45
+ async def get_user(
46
+ user_id: str,
47
+ db: AsyncSession = Depends(get_db)
48
+ ) -> UserResponse:
49
+ """
50
+ Get a user by ID
51
+ """
52
+ user_service = UserService()
53
+ user = await user_service.get_user(db, user_id)
54
+ if not user:
55
+ raise HTTPException(
56
+ status_code=status.HTTP_404_NOT_FOUND,
57
+ detail="User not found"
58
+ )
59
+ return user
60
+
61
+ # POST /users removed - use POST /auth/signup for user registration instead
62
+
63
+ @router.patch(
64
+ "/{user_id}",
65
+ response_model=UserResponse,
66
+ summary="Update user",
67
+ description="Update a user by ID",
68
+ status_code=status.HTTP_200_OK
69
+ )
70
+ async def update_user(
71
+ user_id: str,
72
+ user_data: UserUpdate,
73
+ db: AsyncSession = Depends(get_db)
74
+ ):
75
+ """
76
+ Update a user by ID
77
+
78
+ Args:
79
+ user_id: ID of the user to update
80
+ user_data: User update data
81
+
82
+ Returns:
83
+ Updated UserResponse object
84
+ """
85
+ user_service = UserService()
86
+ try:
87
+ user = await user_service.update_user(db, user_id, user_data)
88
+ if not user:
89
+ raise HTTPException(
90
+ status_code=status.HTTP_404_NOT_FOUND,
91
+ detail="User not found"
92
+ )
93
+ return user
94
+ except BadRequestException as e:
95
+ logger.error(f"Bad request updating user: {e}")
96
+ raise HTTPException(
97
+ status_code=status.HTTP_400_BAD_REQUEST,
98
+ detail=f"Failed to update user: {e.message if hasattr(e, 'message') else str(e)}"
99
+ ) from e
100
+ except Exception as e:
101
+ logger.error(f"Unexpected error updating user: {e}")
102
+ raise HTTPException(
103
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
104
+ detail="An unexpected error occurred while updating the user"
105
+ ) from e
106
+
107
+ @router.delete(
108
+ "/{user_id}",
109
+ summary="Delete user",
110
+ description="Delete a user by ID",
111
+ status_code=status.HTTP_204_NO_CONTENT,
112
+ responses={
113
+ 404: {"description": "User not found"},
114
+ 204: {"description": "User deleted successfully"}
115
+ }
116
+ )
117
+ async def delete_user(
118
+ user_id: str,
119
+ db: AsyncSession = Depends(get_db)
120
+ ):
121
+ """
122
+ Delete a user by ID
123
+
124
+ Args:
125
+ user_id: ID of the user to delete
126
+
127
+ Returns:
128
+ None
129
+ """
130
+ user_service = UserService()
131
+ try:
132
+ deleted = await user_service.delete_user(db, user_id)
133
+ if not deleted:
134
+ raise HTTPException(
135
+ status_code=status.HTTP_404_NOT_FOUND,
136
+ detail="User not found"
137
+ )
138
+ return None
139
+ except Exception as e:
140
+ logger.error(f"Unexpected error deleting user: {e}")
141
+ raise HTTPException(
142
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
143
+ detail="An unexpected error occurred while deleting the user"
144
+ ) from e
@@ -0,0 +1,8 @@
1
+ """
2
+ Pydantic schemas for API request/response models
3
+ """
4
+
5
+ from app.api.v1.schemas.task import TaskCreate, TaskUpdate, TaskResponse
6
+
7
+ __all__ = ["TaskCreate", "TaskUpdate", "TaskResponse"]
8
+
@@ -0,0 +1,198 @@
1
+ from typing import Optional
2
+ from pydantic import BaseModel, Field, field_validator, model_validator
3
+ from datetime import datetime
4
+
5
+ from app.api.v1.schemas.user import WorkOSUserResponse
6
+
7
+ class EmailVerificationRequiredResponse(BaseModel):
8
+ message: str
9
+ pending_authentication_token: str
10
+ email_verification_id: str
11
+ email: str
12
+ requires_verification: bool = True
13
+
14
+ class VerifyEmailRequest(BaseModel):
15
+ pending_authentication_token: str = Field(..., description="Pending authentication token")
16
+ code: str = Field(..., description="Code")
17
+
18
+ class WorkOsVerifyEmailRequest(VerifyEmailRequest):
19
+ ip_address: str = Field(..., description="User IP address")
20
+ user_agent: str = Field(..., description="User user agent")
21
+
22
+
23
+
24
+ class LoginRequest(BaseModel):
25
+ email: str = Field(..., description="User email")
26
+ password: str = Field(..., description="User password")
27
+
28
+ class SignupRequest(BaseModel):
29
+ """
30
+ Schema for user signup/registration.
31
+
32
+ Used for public self-registration endpoint.
33
+ Includes password validation and confirmation.
34
+ """
35
+ email: str = Field(..., min_length=1, max_length=255, description="User email")
36
+ password: str = Field(..., min_length=8, max_length=255, description="User password")
37
+ confirm_password: str = Field(..., min_length=8, max_length=255, description="Password confirmation")
38
+ first_name: Optional[str] = Field(None, max_length=255, description="User first name")
39
+ last_name: Optional[str] = Field(None, max_length=255, description="User last name")
40
+
41
+ @field_validator('password')
42
+ def validate_password(cls, v: str) -> str:
43
+ """Validate password strength."""
44
+ if len(v) < 8:
45
+ raise ValueError("Password must be at least 8 characters long")
46
+ if not any(char.isdigit() for char in v):
47
+ raise ValueError("Password must contain at least one number")
48
+ if not any(char.isalpha() for char in v):
49
+ raise ValueError("Password must contain at least one letter")
50
+ if not any(char.isupper() for char in v):
51
+ raise ValueError("Password must contain at least one uppercase letter")
52
+ if not any(char.islower() for char in v):
53
+ raise ValueError("Password must contain at least one lowercase letter")
54
+ return v
55
+
56
+ @model_validator(mode='after')
57
+ def validate_confirm_password(self) -> 'SignupRequest':
58
+ """Ensure password and confirm_password match."""
59
+ if self.password != self.confirm_password:
60
+ raise ValueError("Password and confirm password do not match")
61
+ return self
62
+
63
+ class WorkOSLoginRequest(LoginRequest):
64
+ ip_address: str = Field(..., description="User IP address")
65
+ user_agent: str = Field(..., description="User user agent")
66
+
67
+ class LoginResponse(BaseModel):
68
+ user: WorkOSUserResponse = Field(..., description="User")
69
+ organization_id: str | None = Field(None, description="Organization ID")
70
+ access_token: str = Field(..., description="Access token")
71
+ refresh_token: str = Field(..., description="Refresh token")
72
+
73
+ class SignupResponse(BaseModel):
74
+ """
75
+ Response for user signup.
76
+
77
+ Returns user information without tokens since email verification is required.
78
+ User must verify email and then login to get tokens.
79
+ """
80
+ user: WorkOSUserResponse = Field(..., description="Created user")
81
+ message: str = Field(default="User created successfully. Please verify your email to login.", description="Success message")
82
+
83
+
84
+ class ForgotPasswordRequest(BaseModel):
85
+ email: str = Field(..., description="User email")
86
+
87
+
88
+ class ForgotPasswordResponse(BaseModel):
89
+ message: str = Field(
90
+ default="If an account exists with this email address, a password reset link has been sent.",
91
+ description="Generic success message"
92
+ )
93
+
94
+ class WorkOSResetPasswordRequest(BaseModel):
95
+ token: str = Field(..., description="Reset password token")
96
+ new_password: str = Field(..., description="New password")
97
+
98
+ class ResetPasswordRequest(WorkOSResetPasswordRequest):
99
+ confirm_new_password: str = Field(..., description="Confirm new password")
100
+
101
+ @field_validator('new_password')
102
+ def validate_new_password(cls, v: str) -> str:
103
+ """Validate new password strength."""
104
+ if len(v) < 8:
105
+ raise ValueError("Password must be at least 8 characters long")
106
+ if not any(char.isdigit() for char in v):
107
+ raise ValueError("Password must contain at least one number")
108
+ if not any(char.isalpha() for char in v):
109
+ raise ValueError("Password must contain at least one letter")
110
+ if not any(char.isupper() for char in v):
111
+ raise ValueError("Password must contain at least one uppercase letter")
112
+ if not any(char.islower() for char in v):
113
+ raise ValueError("Password must contain at least one lowercase letter")
114
+ return v
115
+
116
+ @model_validator(mode='after')
117
+ def validate_confirm_new_password(self) -> 'ResetPasswordRequest':
118
+ """Ensure new password and confirm_new_password match."""
119
+ if self.new_password != self.confirm_new_password:
120
+ raise ValueError("New password and confirm new password do not match")
121
+ return self
122
+
123
+ class WorkOSImpersonatorResponse(BaseModel):
124
+ email: str | None = Field(None, description="Impersonator email")
125
+ reason: str | None = Field(None, description="Impersonation reason")
126
+
127
+
128
+ class VerifyEmailResponse(BaseModel):
129
+ access_token: str | None = Field(None, description="Access token")
130
+ refresh_token: str | None = Field(None, description="Refresh token")
131
+ authentication_method: str | None = Field(None, description="Authentication method")
132
+ impersonator: WorkOSImpersonatorResponse | None = Field(None, description="Impersonator")
133
+ organization_id: str | None = Field(None, description="Organization ID")
134
+ user: WorkOSUserResponse | None = Field(None, description="User")
135
+ sealed_session: str | None = Field(None, description="Sealed session")
136
+
137
+
138
+ class AuthorizationRequest(BaseModel):
139
+ """
140
+ Request to generate OAuth2 authorization URL.
141
+
142
+ Supports two patterns:
143
+ 1. AuthKit: Use provider="authkit" (unified interface)
144
+ 2. SSO: Use connection_id (direct provider connection)
145
+
146
+ Exactly one of provider or connection_id must be provided.
147
+ """
148
+ provider: str | None = Field(
149
+ None,
150
+ description="AuthKit provider (use 'authkit' for unified interface). Mutually exclusive with connection_id."
151
+ )
152
+ connection_id: str | None = Field(
153
+ None,
154
+ description="WorkOS SSO connection ID for direct provider (e.g., Google, Microsoft). Mutually exclusive with provider."
155
+ )
156
+ redirect_uri: str = Field(
157
+ ...,
158
+ description="URI to redirect to after authentication. Must be in allowed list."
159
+ )
160
+ state: str | None = Field(
161
+ None,
162
+ description="Optional state parameter for CSRF protection"
163
+ )
164
+
165
+ @model_validator(mode='after')
166
+ def validate_provider_or_connection(self):
167
+ """Ensure exactly one of provider or connection_id is provided"""
168
+ has_provider = self.provider is not None
169
+ has_connection = self.connection_id is not None
170
+
171
+ if not has_provider and not has_connection:
172
+ raise ValueError("Either 'provider' or 'connection_id' must be provided")
173
+
174
+ if has_provider and has_connection:
175
+ raise ValueError("'provider' and 'connection_id' are mutually exclusive. Provide only one.")
176
+
177
+ return self
178
+
179
+ class AuthorizationUrlResponse(BaseModel):
180
+ authorization_url: str = Field(..., description="Authorization URL")
181
+
182
+ class WorkOSAuthorizationRequest(AuthorizationRequest):
183
+ pass
184
+
185
+ class OAuthCallbackRequest(BaseModel):
186
+ code: str = Field(..., description="Authorization code from OAuth callback")
187
+ state: str | None = Field(None, description="State parameter for CSRF verification")
188
+
189
+ class RefreshTokenRequest(BaseModel):
190
+ refresh_token: str = Field(..., description="Refresh token")
191
+
192
+ class WorkOSRefreshTokenRequest(RefreshTokenRequest):
193
+ ip_address: str = Field(..., description="User IP address")
194
+ user_agent: str = Field(..., description="User user agent")
195
+
196
+ class RefreshTokenResponse(BaseModel):
197
+ access_token: str = Field(..., description="Access token")
198
+ refresh_token: str = Field(..., description="Refresh token")
@@ -0,0 +1,61 @@
1
+ """
2
+ Task Pydantic schemas
3
+ Request and response models for Task API endpoints
4
+ Reference: https://fastapi.tiangolo.com/tutorial/body/
5
+ """
6
+ from datetime import datetime
7
+ from typing import Optional
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class TaskBase(BaseModel):
12
+ """
13
+ Base schema with common Task fields
14
+ Used as base for create and update schemas
15
+ """
16
+ title: str = Field(..., min_length=1, max_length=200, description="Task title")
17
+ description: Optional[str] = Field(None, max_length=1000, description="Task description")
18
+ completed: bool = Field(default=False, description="Whether the task is completed")
19
+
20
+
21
+ class TaskCreate(TaskBase):
22
+ """
23
+ Schema for creating a new task
24
+ Inherits from TaskBase
25
+ """
26
+ pass
27
+
28
+
29
+ class TaskUpdate(BaseModel):
30
+ """
31
+ Schema for updating a task
32
+ All fields are optional for partial updates
33
+ Reference: https://fastapi.tiangolo.com/tutorial/body-updates/
34
+ """
35
+ title: Optional[str] = Field(None, min_length=1, max_length=200, description="Task title")
36
+ description: Optional[str] = Field(None, max_length=1000, description="Task description")
37
+ completed: Optional[bool] = Field(None, description="Whether the task is completed")
38
+
39
+
40
+ class TaskResponse(TaskBase):
41
+ """
42
+ Schema for task response
43
+ Includes all fields from TaskBase plus database-generated fields
44
+ """
45
+ id: int = Field(..., description="Task ID")
46
+ # Timestamps are optional because they may not be immediately available
47
+ # after flush() in serverless environments (database sets them, but asyncpg
48
+ # may not return them without refresh which can cause connection issues)
49
+ created_at: Optional[datetime] = Field(None, description="Timestamp when task was created")
50
+ updated_at: Optional[datetime] = Field(None, description="Timestamp when task was last updated")
51
+
52
+ class Config:
53
+ """
54
+ Pydantic configuration
55
+ Reference: https://docs.pydantic.dev/latest/concepts/config/
56
+ """
57
+ from_attributes = True # Allow creation from ORM objects (SQLAlchemy models)
58
+ json_encoders = {
59
+ datetime: lambda v: v.isoformat() # Convert datetime to ISO format in JSON
60
+ }
61
+
@@ -0,0 +1,96 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+ from uuid import UUID
4
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_serializer, model_validator
5
+
6
+
7
+ class UserBase(BaseModel):
8
+ """
9
+ Base schema with common User fields
10
+ Used as base for create schemas
11
+ """
12
+ first_name: Optional[str] = Field(None, max_length=255, description="User first name")
13
+ last_name: Optional[str] = Field(None, max_length=255, description="User last name")
14
+ email: str = Field(..., min_length=1, max_length=255, description="User email")
15
+ password: str = Field(..., min_length=8, max_length=255, description="User password")
16
+
17
+ class UserCreate(UserBase):
18
+ """
19
+ Schema for creating a new user
20
+ Inherits from UserBase
21
+ Attributes:
22
+ first_name: User first name (optional)
23
+ last_name: User last name (optional)
24
+ email: User email (required)
25
+ password: User password (required)
26
+ confirm_password: User confirm password (required)
27
+ Reference: https://fastapi.tiangolo.com/tutorial/body/
28
+ """
29
+ confirm_password: str = Field(..., min_length=8, max_length=255, description="User confirm password")
30
+
31
+ @field_validator('password')
32
+ def validate_password(cls, v: str) -> str:
33
+ if len(v) < 8:
34
+ raise ValueError("Password must be at least 8 characters long")
35
+ if not any(char.isdigit() for char in v):
36
+ raise ValueError("Password must contain at least one number")
37
+ if not any(char.isalpha() for char in v):
38
+ raise ValueError("Password must contain at least one letter")
39
+ if not any(char.isupper() for char in v):
40
+ raise ValueError("Password must contain at least one uppercase letter")
41
+ if not any(char.islower() for char in v):
42
+ raise ValueError("Password must contain at least one lowercase letter")
43
+ return v
44
+
45
+ @model_validator(mode='after')
46
+ def validate_confirm_password(self) -> 'UserCreate':
47
+ if self.password != self.confirm_password:
48
+ raise ValueError("Password and confirm password do not match")
49
+ return self
50
+
51
+
52
+
53
+ class UserResponse(BaseModel):
54
+ """
55
+ Schema for user response
56
+ Includes all fields from UserBase plus database-generated fields
57
+ Attributes:
58
+ id: User ID (required)
59
+ email: User email (required)
60
+ first_name: User first name (optional)
61
+ last_name: User last name (optional)
62
+ created_at: Timestamp when user was created (optional)
63
+ updated_at: Timestamp when user was last updated (optional)
64
+ Reference: https://fastapi.tiangolo.com/tutorial/response-model/
65
+ """
66
+ id: str = Field(..., description="User ID")
67
+ first_name: Optional[str] = Field(None, max_length=255, description="User first name")
68
+ last_name: Optional[str] = Field(None, max_length=255, description="User last name")
69
+ email: str = Field(..., min_length=1, max_length=255, description="User email")
70
+ created_at: Optional[datetime] = Field(None, description="Timestamp when user was created")
71
+ updated_at: Optional[datetime] = Field(None, description="Timestamp when user was last updated")
72
+
73
+ model_config = ConfigDict(from_attributes=True)
74
+
75
+ class UserUpdate(BaseModel):
76
+ """
77
+ Schema for updating a user
78
+ All fields are optional for partial updates
79
+ """
80
+ first_name: Optional[str] = Field(None, max_length=255, description="User first name")
81
+ last_name: Optional[str] = Field(None, max_length=255, description="User last name")
82
+
83
+
84
+ class WorkOSUserResponse(BaseModel):
85
+ object: str = Field(..., description="Object")
86
+ id: str = Field(..., description="User ID")
87
+ email: str = Field(..., description="User email")
88
+ first_name: str | None = Field(None, description="User first name")
89
+ last_name: str | None = Field(None, description="User last name")
90
+ email_verified: bool = Field(..., description="User email verified")
91
+ profile_picture_url: str | None = Field(None, description="User profile picture URL")
92
+ created_at: datetime = Field(..., description="User created at")
93
+ updated_at: datetime = Field(..., description="User updated at")
94
+
95
+ class Config:
96
+ from_attributes = True
@@ -0,0 +1,4 @@
1
+ """
2
+ Core application configuration and utilities
3
+ """
4
+