Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
C
Chart Analyzer
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
zeinab.rostom
Chart Analyzer
Commits
8f656a91
Commit
8f656a91
authored
Jul 31, 2025
by
ZeinabRm13
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add filesystem storage
parent
59fc0d8b
Changes
16
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
175 additions
and
89 deletions
+175
-89
main.cpython-312.pyc
src/app/__pycache__/main.cpython-312.pyc
+0
-0
auth.cpython-312.pyc
src/app/api/fastapi/routes/__pycache__/auth.cpython-312.pyc
+0
-0
main.py
src/app/main.py
+1
-1
authentication_service_port.py
src/application/ports/authentication_service_port.py
+32
-5
file_storage_service.py
src/application/ports/file_storage_service.py
+46
-0
authentication_service.py
src/application/services/authentication_service.py
+41
-31
__init__.py
src/infrastructure/persistence/models/__init__.py
+6
-0
analysis_model.py
src/infrastructure/persistence/models/analysis_model.py
+5
-8
base.py
src/infrastructure/persistence/models/base.py
+9
-0
blacklisted_token_model.py
...rastructure/persistence/models/blacklisted_token_model.py
+2
-5
chart_model.py
src/infrastructure/persistence/models/chart_model.py
+8
-9
conversation_models.py
src/infrastructure/persistence/models/conversation_models.py
+18
-20
user_model.py
src/infrastructure/persistence/models/user_model.py
+7
-10
chartGemma_service.py
src/infrastructure/services/chartGemma_service.py
+0
-0
file_storage_service.py
src/infrastructure/services/file_storage_service.py
+0
-0
ollama_service.py
src/infrastructure/services/ollama_service.py
+0
-0
No files found.
src/app/__pycache__/main.cpython-312.pyc
0 → 100644
View file @
8f656a91
File added
src/app/api/fastapi/routes/__pycache__/auth.cpython-312.pyc
View file @
8f656a91
No preview for this file type
src/app/main.py
View file @
8f656a91
from
fastapi
import
FastAPI
from
src.infrastructure.
api.fastapi.routes
import
auth
,
charts
from
api.fastapi.routes
import
auth
,
charts
from
fastapi.middleware.cors
import
CORSMiddleware
from
sqlalchemy
import
create_engine
from
sqlalchemy.exc
import
OperationalError
...
...
src/application/ports/authentication_service_port.py
View file @
8f656a91
from
abc
import
ABC
,
abstractmethod
from
src.domain.entities.user
import
User
from
src.application.dtos.authentication
import
(
RegisterRequestDTO
,
LoginRequestDTO
,
...
...
@@ -10,7 +8,36 @@ from src.application.dtos.authentication import (
class
AuthServicePort
(
ABC
):
@
abstractmethod
async
def
register
(
self
,
email
,
password
)
->
User
:
...
async
def
register
(
self
,
register_dto
:
RegisterRequestDTO
)
->
UserResponseDTO
:
"""
Register a new user with email and password
:param register_dto: RegisterRequestDTO containing email and password
:return: UserResponseDTO containing user details
"""
...
@
abstractmethod
async
def
login
(
self
,
login_dto
:
LoginRequestDTO
)
->
TokenResponseDTO
:
"""
Authenticate user and return JWT token
:param login_dto: LoginRequestDTO containing email and password
:return: TokenResponseDTO containing access token
"""
...
@
abstractmethod
async
def
logout
(
self
,
token
:
str
)
->
None
:
"""
Invalidate a JWT token
:param token: JWT token to invalidate
"""
...
@
abstractmethod
async
def
login
(
self
,
email
,
password
)
->
str
:
...
\ No newline at end of file
async
def
validate_token
(
self
,
token
:
str
)
->
bool
:
"""
Validate if a JWT token is still valid
:param token: JWT token to validate
:return: Boolean indicating token validity
"""
...
\ No newline at end of file
src/application/ports/file_storage_service.py
0 → 100644
View file @
8f656a91
from
abc
import
ABC
,
abstractmethod
from
uuid
import
UUID
from
typing
import
Tuple
,
Optional
from
datetime
import
datetime
class
FileStoragePort
(
ABC
):
"""Abstract base class for file storage operations"""
@
abstractmethod
async
def
save_chart_image
(
self
,
user_id
:
UUID
,
image_data
:
bytes
,
content_type
:
str
)
->
str
:
"""Save chart image to storage and return file path"""
pass
@
abstractmethod
async
def
get_chart_image
(
self
,
file_path
:
str
)
->
Tuple
[
bytes
,
str
]:
"""Retrieve chart image data and content type"""
pass
@
abstractmethod
async
def
generate_thumbnail
(
self
,
original_path
:
str
,
output_path
:
str
,
dimensions
:
Tuple
[
int
,
int
]
=
(
300
,
300
)
)
->
str
:
"""Generate and save thumbnail version"""
pass
@
abstractmethod
async
def
delete_file
(
self
,
file_path
:
str
)
->
bool
:
"""Delete a stored file"""
pass
@
abstractmethod
def
generate_file_path
(
self
,
user_id
:
UUID
,
extension
:
str
)
->
str
:
"""Generate storage path for a new file"""
pass
\ No newline at end of file
src/application/services/authentication_service.py
View file @
8f656a91
# src/application/services/auth.py
from
datetime
import
datetime
,
timezone
,
timedelta
from
jose
import
jwt
from
jose
import
jwt
,
JWTError
from
passlib.context
import
CryptContext
from
src.domain.ports.repositories.user_repository
import
UserRepositoryPort
from
src.domain.ports.repositories.token_repository
import
TokenRepositoryPort
...
...
@@ -15,15 +15,11 @@ from src.application.dtos.authentication import (
TokenResponseDTO
)
# Update src/application/services/auth.py
from
jose
import
JWTError
,
jwt
from
datetime
import
datetime
,
timezone
,
timedelta
class
AuthService
(
AuthServicePort
):
def
__init__
(
self
,
user_repo
:
UserRepositoryPort
,
token_repo
:
TokenRepositoryPort
,
# Add this
token_repo
:
TokenRepositoryPort
,
secret_key
:
str
=
Settings
()
.
JWT_SECRET
,
algorithm
:
str
=
"HS256"
,
expires_minutes
:
int
=
30
...
...
@@ -35,8 +31,38 @@ class AuthService(AuthServicePort):
self
.
_expires_minutes
=
expires_minutes
self
.
_pwd_context
=
CryptContext
(
schemes
=
[
"bcrypt"
],
deprecated
=
"auto"
)
async
def
register
(
self
,
register_dto
:
RegisterRequestDTO
)
->
UserResponseDTO
:
"""
Register a new user with email and password
Returns UserResponseDTO containing user details
"""
if
await
self
.
_user_repo
.
get_by_email
(
register_dto
.
email
):
raise
ValueError
(
"Email already registered"
)
user
=
User
(
id
=
str
(
uuid
.
uuid4
()),
email
=
register_dto
.
email
,
password_hash
=
self
.
_hash_password
(
register_dto
.
password
),
is_active
=
True
)
await
self
.
_user_repo
.
create_user
(
user
)
return
self
.
_user_to_dto
(
user
)
async
def
login
(
self
,
login_dto
:
LoginRequestDTO
)
->
TokenResponseDTO
:
"""
Authenticate user and return JWT token
Returns TokenResponseDTO containing access token
"""
user
=
await
self
.
_user_repo
.
get_by_email
(
login_dto
.
email
)
if
not
user
or
not
self
.
_verify_password
(
login_dto
.
password
,
user
.
password_hash
):
raise
ValueError
(
"Invalid credentials"
)
access_token
=
self
.
_create_access_token
(
user
.
email
)
return
TokenResponseDTO
(
access_token
=
access_token
)
async
def
logout
(
self
,
token
:
str
)
->
None
:
"""Invalidate a JWT token"""
"""Invalidate a JWT token
by adding it to blacklist
"""
try
:
payload
=
jwt
.
decode
(
token
,
self
.
_secret_key
,
algorithms
=
[
self
.
_algorithm
])
exp
=
payload
.
get
(
"exp"
)
...
...
@@ -47,7 +73,10 @@ class AuthService(AuthServicePort):
pass
# Token is invalid anyway
async
def
validate_token
(
self
,
token
:
str
)
->
bool
:
"""Check if token is valid and not blacklisted"""
"""
Check if token is valid and not blacklisted
Returns boolean indicating token validity
"""
try
:
if
await
self
.
_token_repo
.
is_blacklisted
(
token
):
return
False
...
...
@@ -57,36 +86,16 @@ class AuthService(AuthServicePort):
except
JWTError
:
return
False
async
def
register
(
self
,
email
,
password
)
->
User
:
if
await
self
.
_user_repo
.
get_by_email
(
email
):
raise
ValueError
(
"Email already registered"
)
user
=
User
(
id
=
str
(
uuid
.
uuid4
()),
email
=
email
,
password_hash
=
self
.
_hash_password
(
password
)
)
await
self
.
_user_repo
.
create_user
(
user
)
return
user
async
def
login
(
self
,
email
:
str
,
password
:
str
)
->
str
:
user
=
await
self
.
_user_repo
.
get_by_email
(
email
)
if
not
user
or
not
self
.
_verify_password
(
password
,
user
.
password_hash
):
raise
ValueError
(
"Invalid credentials"
)
# Return just the token string, not the whole response
return
self
.
_create_access_token
(
user
.
email
)
def
_hash_password
(
self
,
password
:
str
)
->
str
:
"""Hash password using bcrypt"""
return
self
.
_pwd_context
.
hash
(
password
)
def
_verify_password
(
self
,
plain_password
:
str
,
hashed_password
:
str
)
->
bool
:
"""Verify password against stored hash"""
return
self
.
_pwd_context
.
verify
(
plain_password
,
hashed_password
)
def
_create_access_token
(
self
,
email
:
str
)
->
str
:
"""Create JWT token with expiration"""
expires
=
datetime
.
now
(
timezone
.
utc
)
+
timedelta
(
minutes
=
self
.
_expires_minutes
)
return
jwt
.
encode
(
{
"sub"
:
email
,
"exp"
:
expires
},
...
...
@@ -95,6 +104,7 @@ class AuthService(AuthServicePort):
)
def
_user_to_dto
(
self
,
user
:
User
)
->
UserResponseDTO
:
"""Convert User entity to UserResponseDTO"""
return
UserResponseDTO
(
id
=
user
.
id
,
email
=
user
.
email
,
...
...
src/infrastructure/persistence/models/__init__.py
0 → 100644
View file @
8f656a91
from
src.infrastructure.persistence.models.base
import
Base
from
src.infrastructure.persistence.models.user_model
import
UserModel
from
src.infrastructure.persistence.models.chart_model
import
ChartImageModel
from
src.infrastructure.persistence.models.conversation_models
import
ConversationModel
,
ConversationMessageModel
from
src.infrastructure.persistence.models.blacklisted_token_model
import
BlacklistedTokenModel
from
src.infrastructure.persistence.models.analysis_model
import
ChartAnalysisModel
\ No newline at end of file
src/infrastructure/persistence/models/analysis_model.py
View file @
8f656a91
...
...
@@ -4,17 +4,14 @@ from sqlalchemy.orm import relationship
from
sqlalchemy.dialects.postgresql
import
UUID
import
uuid
class
ChartAnalysis
(
Base
):
__tablename__
=
'chart_analys
e
s'
class
ChartAnalysis
Model
(
Base
,
TimestampMixin
):
__tablename__
=
'chart_analys
i
s'
id
=
Column
(
UUID
(
as_uuid
=
True
),
primary_key
=
True
,
default
=
uuid
.
uuid
4
)
id
=
Column
(
UUID
(
as_uuid
=
True
),
primary_key
=
True
,
default
=
uuid4
)
chart_image_id
=
Column
(
UUID
(
as_uuid
=
True
),
ForeignKey
(
'chart_images.id'
),
nullable
=
False
)
question
=
Column
(
Text
,
nullable
=
False
)
answer
=
Column
(
Text
,
nullable
=
False
)
created_at
=
Column
(
DateTime
(
timezone
=
True
),
server_default
=
func
.
now
())
metadata
=
Column
(
JSONB
)
# Added for additional analysis data
# Relationship
chart_image
=
relationship
(
"ChartImage"
,
back_populates
=
"analyses"
)
def
__repr__
(
self
):
return
f
"<ChartAnalysis(id={self.id}, chart_image_id={self.chart_image_id})>"
chart_image
=
relationship
(
"ChartImageModel"
,
back_populates
=
"analysis"
)
src/infrastructure/persistence/models/base.py
0 → 100644
View file @
8f656a91
from
sqlalchemy.ext.declarative
import
declarative_base
from
sqlalchemy
import
Column
,
DateTime
,
func
from
datetime
import
datetime
,
timezone
Base
=
declarative_base
()
class
TimestampMixin
:
created_at
=
Column
(
DateTime
(
timezone
=
True
),
server_default
=
func
.
now
())
updated_at
=
Column
(
DateTime
(
timezone
=
True
),
onupdate
=
func
.
now
())
\ No newline at end of file
src/infrastructure/persistence/models/blacklisted_token_model.py
View file @
8f656a91
...
...
@@ -7,13 +7,10 @@ from sqlalchemy.dialects.postgresql import UUID
import
uuid
class
BlacklistedToken
(
Base
):
class
BlacklistedToken
Model
(
Base
):
__tablename__
=
'blacklisted_tokens'
id
=
Column
(
UUID
(
as_uuid
=
True
),
primary_key
=
True
,
default
=
uuid
.
uuid
4
)
id
=
Column
(
UUID
(
as_uuid
=
True
),
primary_key
=
True
,
default
=
uuid4
)
token
=
Column
(
String
(
512
),
unique
=
True
,
nullable
=
False
)
expires_at
=
Column
(
DateTime
(
timezone
=
True
),
nullable
=
False
)
created_at
=
Column
(
DateTime
(
timezone
=
True
),
server_default
=
func
.
now
())
def
__repr__
(
self
):
return
f
"<BlacklistedToken(token={self.token[:10]}...)>"
\ No newline at end of file
src/infrastructure/persistence/models/chart_model.py
View file @
8f656a91
...
...
@@ -7,16 +7,15 @@ from sqlalchemy.dialects.postgresql import UUID
import
uuid
class
ChartImage
(
Base
):
class
ChartImage
Model
(
Base
,
TimestampMixin
):
__tablename__
=
'chart_images'
id
=
Column
(
UUID
(
as_uuid
=
True
),
primary_key
=
True
,
default
=
uuid
.
uuid
4
)
id
=
Column
(
UUID
(
as_uuid
=
True
),
primary_key
=
True
,
default
=
uuid4
)
user_id
=
Column
(
UUID
(
as_uuid
=
True
),
ForeignKey
(
'users.id'
),
nullable
=
False
)
image_data
=
Column
(
LargeBinary
,
nullable
=
False
)
# For storing binary data
uploaded_at
=
Column
(
DateTime
(
timezone
=
True
),
server_default
=
func
.
now
()
)
file_path
=
Column
(
String
(
512
),
nullable
=
False
)
# Changed from image_data to file_path
thumbnail_path
=
Column
(
String
(
512
),
nullable
=
True
)
# Relationship
analyses
=
relationship
(
"ChartAnalysis"
,
back_populates
=
"chart_image"
)
def
__repr__
(
self
):
return
f
"<ChartImage(id={self.id}, user_id={self.user_id})>"
\ No newline at end of file
# Relationships
user
=
relationship
(
"UserModel"
,
back_populates
=
"chart_images"
)
analysis
=
relationship
(
"ChartAnalysisModel"
,
back_populates
=
"chart_image"
)
conversations
=
relationship
(
"ConversationModel"
,
back_populates
=
"chart_image"
)
src/infrastructure/persistence/models/conversation_models.py
View file @
8f656a91
...
...
@@ -3,32 +3,30 @@ from sqlalchemy.orm import relationship
from
sqlalchemy.dialects.postgresql
import
UUID
from
src.infrastructure.persistence.models.base
import
Base
from
datetime
import
datetime
,
timezone
class
ConversationModel
(
Base
):
"""SQLAlchemy model for conversations"""
class
ConversationModel
(
Base
,
TimestampMixin
):
__tablename__
=
"conversations"
id
=
Column
(
UUID
(
as_uuid
=
True
),
primary_key
=
True
)
user_id
=
Column
(
UUID
(
as_uuid
=
True
),
nullable
=
False
)
chart_image_id
=
Column
(
UUID
(
as_uuid
=
True
),
nullable
=
Fals
e
)
id
=
Column
(
UUID
(
as_uuid
=
True
),
primary_key
=
True
,
default
=
uuid4
)
user_id
=
Column
(
UUID
(
as_uuid
=
True
),
ForeignKey
(
'users.id'
),
nullable
=
False
)
chart_image_id
=
Column
(
UUID
(
as_uuid
=
True
),
ForeignKey
(
'chart_images.id'
),
nullable
=
Tru
e
)
title
=
Column
(
String
(
255
),
nullable
=
False
)
created_at
=
Column
(
DateTime
(
timezone
=
True
),
default
=
lambda
:
datetime
.
now
(
timezone
.
utc
))
updated_at
=
Column
(
DateTime
(
timezone
=
True
),
default
=
lambda
:
datetime
.
now
(
timezone
.
utc
),
onupdate
=
lambda
:
datetime
.
now
(
timezone
.
utc
))
is_active
=
Column
(
Boolean
,
default
=
True
)
# Relationship to messages
messages
=
relationship
(
"ConversationMessageModel"
,
back_populates
=
"conversation"
,
cascade
=
"all, delete-orphan"
)
# Relationships
user
=
relationship
(
"UserModel"
,
back_populates
=
"conversations"
)
chart_image
=
relationship
(
"ChartImageModel"
,
back_populates
=
"conversations"
)
messages
=
relationship
(
"ConversationMessageModel"
,
back_populates
=
"conversation"
,
cascade
=
"all, delete-orphan"
)
class
ConversationMessageModel
(
Base
):
"""SQLAlchemy model for conversation messages"""
__tablename__
=
"conversation_messages"
id
=
Column
(
UUID
(
as_uuid
=
True
),
primary_key
=
True
)
id
=
Column
(
UUID
(
as_uuid
=
True
),
primary_key
=
True
,
default
=
uuid4
)
conversation_id
=
Column
(
UUID
(
as_uuid
=
True
),
ForeignKey
(
"conversations.id"
),
nullable
=
False
)
user_id
=
Column
(
UUID
(
as_uuid
=
True
),
nullable
=
False
)
message_type
=
Column
(
String
(
20
),
nullable
=
False
)
# 'user' or 'assistant'
content
=
Column
(
Text
,
nullable
=
False
)
timestamp
=
Column
(
DateTime
(
timezone
=
True
),
default
=
lambda
:
datetime
.
now
(
timezone
.
utc
))
# Relationship to conversation
conversation
=
relationship
(
"ConversationModel"
,
back_populates
=
"messages"
)
\ No newline at end of file
created_at
=
Column
(
DateTime
(
timezone
=
True
),
server_default
=
func
.
now
())
metadata
=
Column
(
JSONB
)
# For additional message data
# Relationship
conversation
=
relationship
(
"ConversationModel"
,
back_populates
=
"messages"
)
\ No newline at end of file
src/infrastructure/persistence/models/user_model.py
View file @
8f656a91
...
...
@@ -4,20 +4,17 @@ from sqlalchemy.orm import declarative_base
from
sqlalchemy.orm
import
relationship
from
sqlalchemy.dialects.postgresql
import
UUID
import
uuid
from
.base
import
Base
,
TimestampMixin
# 1. Create a Base class that all your models will inherit from.
Base
=
declarative_base
()
class
User
(
Base
):
class
UserModel
(
Base
,
TimestampMixin
):
__tablename__
=
'users'
id
=
Column
(
UUID
(
as_uuid
=
True
),
primary_key
=
True
,
default
=
uuid
.
uuid
4
)
id
=
Column
(
UUID
(
as_uuid
=
True
),
primary_key
=
True
,
default
=
uuid4
)
email
=
Column
(
String
(
255
),
unique
=
True
,
nullable
=
False
)
password_hash
=
Column
(
String
(
255
),
nullable
=
False
)
# Store only hashed passwords
password_hash
=
Column
(
String
(
255
),
nullable
=
False
)
is_active
=
Column
(
Boolean
,
default
=
True
)
created_at
=
Column
(
DateTime
(
timezone
=
True
),
server_default
=
func
.
now
())
last_login
=
Column
(
DateTime
(
timezone
=
True
),
nullable
=
True
)
def
__repr__
(
self
):
return
f
"<User(id={self.id}, email={self.email})>"
# Relationships
chart_images
=
relationship
(
"ChartImageModel"
,
back_populates
=
"user"
)
conversations
=
relationship
(
"ConversationModel"
,
back_populates
=
"user"
)
src/
application
/services/chartGemma_service.py
→
src/
infrastructure
/services/chartGemma_service.py
View file @
8f656a91
File moved
src/infrastructure/services/file_storage_service.py
0 → 100644
View file @
8f656a91
src/
application
/services/ollama_service.py
→
src/
infrastructure
/services/ollama_service.py
View file @
8f656a91
File moved
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment