iBrokeTheCode commited on
Commit
459b8f5
Β·
1 Parent(s): 67919d4

chore: Add model UI files

Browse files
api/Dockerfile CHANGED
@@ -8,6 +8,7 @@ WORKDIR /src
8
 
9
  RUN pip install --upgrade pip && pip install -r requirements.txt
10
 
 
11
  COPY ./ /src/
12
 
13
  FROM base AS test
 
8
 
9
  RUN pip install --upgrade pip && pip install -r requirements.txt
10
 
11
+ # πŸ‘‡ Uncomment for DEPLOY / Comment for DevContainers
12
  COPY ./ /src/
13
 
14
  FROM base AS test
api/app/user/router.py CHANGED
@@ -1,4 +1,3 @@
1
- # from typing import List
2
  from app import db
3
  from app.auth.jwt import get_current_user
4
  from fastapi import APIRouter, Depends, HTTPException, status
 
 
1
  from app import db
2
  from app.auth.jwt import get_current_user
3
  from fastapi import APIRouter, Depends, HTTPException, status
api/tests/test_router_model.py CHANGED
@@ -2,11 +2,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
2
 
3
  import pytest
4
  from app.auth.jwt import get_current_user
5
-
6
- # from app.model.schema import PredictResponse
7
  from fastapi import UploadFile
8
-
9
- # from fastapi.testclient import TestClient
10
  from httpx import AsyncClient
11
  from main import app
12
 
 
2
 
3
  import pytest
4
  from app.auth.jwt import get_current_user
 
 
5
  from fastapi import UploadFile
 
 
6
  from httpx import AsyncClient
7
  from main import app
8
 
api/tests/test_utils.py CHANGED
@@ -1,4 +1,3 @@
1
- # import os
2
  from io import BytesIO
3
 
4
  import app.utils as utils
 
 
1
  from io import BytesIO
2
 
3
  import app.utils as utils
model/Dockerfile CHANGED
@@ -7,6 +7,7 @@ RUN pip3 install -r requirements.txt
7
 
8
  ENV PYTHONPATH=$PYTHONPATH:/src/
9
 
 
10
  COPY ./ /src/
11
 
12
  WORKDIR /src
 
7
 
8
  ENV PYTHONPATH=$PYTHONPATH:/src/
9
 
10
+ # πŸ‘‡ Uncomment for DEPLOY / Comment for DevContainers
11
  COPY ./ /src/
12
 
13
  WORKDIR /src
ui/Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from python:3.8.13 AS base
2
+
3
+ ENV PYTHONPATH=$PYTHONPATH:/src/
4
+
5
+ COPY ./requirements.txt /src/requirements.txt
6
+
7
+ WORKDIR /src
8
+
9
+ RUN pip install --upgrade pip && pip install -r requirements.txt
10
+
11
+ # πŸ‘‡ Uncomment for DEPLOY / Comment for DevContainers
12
+ COPY ./ /src/
13
+
14
+ FROM base AS test
15
+ RUN ["python", "-m", "pytest", "-v", "tests"]
16
+
17
+ FROM base AS build
18
+ CMD streamlit run --server.port 9090 app/image_classifier_app.py
ui/app/image_classifier_app.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+
3
+ import requests
4
+ import streamlit as st
5
+ from app.settings import API_BASE_URL
6
+ from PIL import Image
7
+
8
+
9
+ def login(username: str, password: str) -> Optional[str]:
10
+ """This function calls the login endpoint of the API to authenticate the user
11
+ and get a token.
12
+
13
+ Args:
14
+ username (str): email of the user
15
+ password (str): password of the user
16
+
17
+ Returns:
18
+ Optional[str]: token if login is successful, None otherwise
19
+ """
20
+ # Construct the API endpoint URL
21
+ url = "{}/{}".format(API_BASE_URL, "login")
22
+
23
+ # Set up the request headers
24
+ headers = {
25
+ "accept": "application/json",
26
+ "Content-Type": "application/x-www-form-urlencoded",
27
+ }
28
+
29
+ # Prepare the data payload
30
+ data = {
31
+ "username": username, # Required
32
+ "password": password, # Required
33
+ "grant_type": "", # Optionals
34
+ "scope": "",
35
+ "client_id": "",
36
+ "client_secret": "",
37
+ }
38
+
39
+ # Send the API request
40
+ response = requests.post(url, headers=headers, data=data)
41
+ if response.status_code == 200:
42
+ return response.json().get("access_token")
43
+
44
+ return None
45
+
46
+
47
+ def predict(token: str, uploaded_file: Image) -> requests.Response:
48
+ """This function calls the predict endpoint of the API to classify the uploaded
49
+ image.
50
+
51
+ Args:
52
+ token (str): token to authenticate the user
53
+ uploaded_file (Image): image to classify
54
+
55
+ Returns:
56
+ requests.Response: response from the API
57
+ """
58
+ # Create a dictionary with the file data
59
+ files = {"file": (uploaded_file.name, uploaded_file.getvalue())}
60
+
61
+ # Add the token to the headers
62
+ headers = {"Authorization": f"Bearer {token}"}
63
+
64
+ # Make a POST request to the predict endpoint
65
+ url = f"{API_BASE_URL}/model/predict"
66
+ response = requests.post(url, files=files, headers=headers)
67
+
68
+ return response
69
+
70
+
71
+ def send_feedback(
72
+ token: str, feedback: str, score: float, prediction: str, image_file_name: str
73
+ ) -> requests.Response:
74
+ """This function calls the feedback endpoint of the API to send feedback about
75
+ the classification.
76
+
77
+ Args:
78
+ token (str): token to authenticate the user
79
+ feedback (str): string with feedback
80
+ score (float): confidence score of the prediction
81
+ prediction (str): predicted class
82
+ image_file_name (str): name of the image file
83
+
84
+ Returns:
85
+ requests.Response: _description_
86
+ """
87
+ # Create a dictionary with the feedback data including feedback, score, predicted_class, and image_file_name
88
+ data = {
89
+ "feedback": feedback,
90
+ "score": score,
91
+ "predicted_class": prediction,
92
+ "image_file_name": image_file_name,
93
+ }
94
+
95
+ # Add the token to the headers
96
+ headers = {"Authorization": f"Bearer {token}"}
97
+
98
+ # Make a POST request to the feedback endpoint
99
+ url = f"{API_BASE_URL}/feedback"
100
+ response = requests.post(url, json=data, headers=headers)
101
+
102
+ return response
103
+
104
+
105
+ # User Interface
106
+ st.set_page_config(page_title="Image Classifier", page_icon="πŸ“·")
107
+ st.markdown(
108
+ "<h1 style='text-align: center; color: #4B89DC;'>Image Classifier</h1>",
109
+ unsafe_allow_html=True,
110
+ )
111
+
112
+ # Login Form
113
+ if "token" not in st.session_state:
114
+ st.markdown("## Login")
115
+ username = st.text_input("Username")
116
+ password = st.text_input("Password", type="password")
117
+ if st.button("Login"):
118
+ token = login(username, password)
119
+ if token:
120
+ st.session_state.token = token
121
+ st.success("Login successful!")
122
+ st.rerun() # Refresh UI
123
+ else:
124
+ st.error("Login failed. Please check your credentials.")
125
+ else:
126
+ st.success("You are logged in!")
127
+
128
+
129
+ if "token" in st.session_state:
130
+ token = st.session_state.token
131
+
132
+ # Load image
133
+ uploaded_file = st.file_uploader("Sube una imagen", type=["jpg", "jpeg", "png"])
134
+
135
+ print(type(uploaded_file))
136
+
137
+ # Display scaled image if uploaded
138
+ if uploaded_file is not None:
139
+ image = Image.open(uploaded_file)
140
+ st.image(image, caption="Imagen subida", width=300)
141
+
142
+ if "classification_done" not in st.session_state:
143
+ st.session_state.classification_done = False
144
+
145
+ # Classification button
146
+ if st.button("Classify"):
147
+ if uploaded_file is not None:
148
+ response = predict(token, uploaded_file)
149
+ if response.status_code == 200:
150
+ result = response.json()
151
+ st.write(f"**Prediction:** {result['prediction']}")
152
+ st.write(f"**Score:** {result['score']}")
153
+ st.session_state.classification_done = True
154
+ st.session_state.result = result
155
+ else:
156
+ st.error("Error classifying image. Please try again.")
157
+ else:
158
+ st.warning("Please upload an image before classifying.")
159
+
160
+ # Display feedback field only if image has been classified
161
+ if st.session_state.classification_done:
162
+ st.markdown("## Feedback")
163
+ feedback = st.text_area("If the prediction was wrong, please provide feedback.")
164
+ if st.button("Send Feedback"):
165
+ if feedback:
166
+ token = st.session_state.token
167
+ result = st.session_state.result
168
+ score = result["score"]
169
+ prediction = result["prediction"]
170
+ image_file_name = result.get("image_file_name", "uploaded_image")
171
+ response = send_feedback(
172
+ token, feedback, score, prediction, image_file_name
173
+ )
174
+ if response.status_code == 201:
175
+ st.success("Thanks for your feedback!")
176
+ else:
177
+ st.error("Error sending feedback. Please try again.")
178
+ else:
179
+ st.warning("Please provide feedback before sending.")
180
+ st.warning("Please provide feedback before sending.")
181
+
182
+ # Footer
183
+ st.markdown("<hr style='border:2px solid #4B89DC;'>", unsafe_allow_html=True)
184
+ st.markdown(
185
+ "<p style='text-align: center; color: #4B89DC;'>2024 Image Classifier App</p>",
186
+ unsafe_allow_html=True,
187
+ )
ui/app/settings.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import os
2
+
3
+ API_HOST = os.getenv("API_HOST", "localhost")
4
+ API_PORT = os.getenv("API_PORT", 8000)
5
+ API_BASE_URL = f"http://{API_HOST}:{API_PORT}"
ui/requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ streamlit
2
+ pytest
ui/tests/dog.jpeg ADDED

Git LFS Details

  • SHA256: 42c39376d6a11181dacdcd61670431056bef7feba55c37cb70ff45147ffd092e
  • Pointer size: 129 Bytes
  • Size of remote file: 6.58 kB
ui/tests/test_image_classifier_app.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import unittest
3
+ from io import BytesIO
4
+ from unittest import mock
5
+
6
+ import app.image_classifier_app as ui_app
7
+ from PIL import Image
8
+
9
+ path_tests = os.path.dirname(os.path.abspath(__file__))
10
+
11
+
12
+ class TestMLService(unittest.TestCase):
13
+ # πŸ’‘ NOTE Run tests with: python tests/test_image_classifier_app.py
14
+ def setUp(self):
15
+ self.token = "dummy_token"
16
+ self.image_file = Image.open(path_tests + "/dog.jpeg")
17
+ self.uploaded_file = mock.MagicMock(spec=BytesIO)
18
+ self.uploaded_file.getvalue.return_value = BytesIO()
19
+ self.uploaded_file.name = "dog.jpeg"
20
+ self.image_file.save(self.uploaded_file, format="JPEG")
21
+ self.headers = {"Authorization": f"Bearer {self.token}"}
22
+
23
+ # python3 -m unittest -vvv tests.test_model
24
+ def test_login_success(self):
25
+ # πŸ’‘ NOTE Run test with: python -m unittest -vvv tests.test_image_classifier_app.TestMLService.test_login_success
26
+ expected_token = "dummy_token"
27
+ response_data = {"access_token": expected_token}
28
+ with mock.patch("requests.post") as mock_post:
29
+ mock_post.return_value.status_code = 200
30
+ mock_post.return_value.json.return_value = response_data
31
+
32
+ token = ui_app.login("username", "password")
33
+
34
+ headers = {
35
+ "accept": "application/json",
36
+ "Content-Type": "application/x-www-form-urlencoded",
37
+ }
38
+
39
+ self.assertEqual(token, expected_token)
40
+ mock_post.assert_called_once_with(
41
+ ui_app.API_BASE_URL + "/login",
42
+ headers=headers,
43
+ data={
44
+ "grant_type": "",
45
+ "username": "username",
46
+ "password": "password",
47
+ "scope": "",
48
+ "client_id": "",
49
+ "client_secret": "",
50
+ },
51
+ )
52
+
53
+ def test_login_failure(self):
54
+ # πŸ’‘ NOTE Run test with: python -m unittest -vvv tests.test_image_classifier_app.TestMLService.test_login_failure
55
+ with mock.patch("requests.post") as mock_post:
56
+ mock_post.return_value.status_code = 401
57
+
58
+ token = ui_app.login("username", "password")
59
+
60
+ headers = {
61
+ "accept": "application/json",
62
+ "Content-Type": "application/x-www-form-urlencoded",
63
+ }
64
+
65
+ self.assertIsNone(token)
66
+ mock_post.assert_called_once_with(
67
+ ui_app.API_BASE_URL + "/login",
68
+ headers=headers,
69
+ data={
70
+ "grant_type": "",
71
+ "username": "username",
72
+ "password": "password",
73
+ "scope": "",
74
+ "client_id": "",
75
+ "client_secret": "",
76
+ },
77
+ )
78
+
79
+ def test_predict_success(self):
80
+ # πŸ’‘ NOTE Run test with: python -m unittest -vvv tests.test_image_classifier_app.TestMLService.test_predict_success
81
+ expected_response = {"prediction": "Eskimo_dog", "score": 0.9346}
82
+ with mock.patch("requests.post") as mock_post:
83
+ mock_post.return_value.status_code = 200
84
+ mock_post.return_value.json.return_value = expected_response
85
+
86
+ response = ui_app.predict(self.token, self.uploaded_file)
87
+
88
+ self.assertEqual(response.status_code, 200)
89
+ self.assertEqual(response.json(), expected_response)
90
+ mock_post.assert_called_once_with(
91
+ ui_app.API_BASE_URL + "/model/predict",
92
+ files={
93
+ "file": (self.uploaded_file.name, self.uploaded_file.getvalue())
94
+ },
95
+ headers=self.headers,
96
+ )
97
+
98
+ def test_predict_failure(self):
99
+ # πŸ’‘ NOTE Run test with: python -m unittest -vvv tests.test_image_classifier_app.TestMLService.test_predict_failure
100
+ with mock.patch("requests.post") as mock_post:
101
+ mock_post.return_value.status_code = 500
102
+
103
+ response = ui_app.predict(self.token, self.uploaded_file)
104
+
105
+ self.assertEqual(response.status_code, 500)
106
+ mock_post.assert_called_once_with(
107
+ ui_app.API_BASE_URL + "/model/predict",
108
+ files={
109
+ "file": (self.uploaded_file.name, self.uploaded_file.getvalue())
110
+ },
111
+ headers=self.headers,
112
+ )
113
+
114
+ def test_send_feedback_success(self):
115
+ # πŸ’‘ NOTE Run test with: python -m unittest -vvv tests.test_image_classifier_app.TestMLService.test_send_feedback_success
116
+ expected_response = {"status": "success"}
117
+ feedback = "This is a feedback"
118
+ score = 0.9346
119
+ prediction = "Eskimo_dog"
120
+ image_file_name = "dog.jpeg"
121
+ with mock.patch("requests.post") as mock_post:
122
+ mock_post.return_value.status_code = 201
123
+ mock_post.return_value.json.return_value = expected_response
124
+
125
+ response = ui_app.send_feedback(
126
+ self.token, feedback, score, prediction, image_file_name
127
+ )
128
+
129
+ self.assertEqual(response.status_code, 201)
130
+ self.assertEqual(response.json(), expected_response)
131
+ mock_post.assert_called_once_with(
132
+ ui_app.API_BASE_URL + "/feedback",
133
+ json={
134
+ "feedback": feedback,
135
+ "score": score,
136
+ "predicted_class": prediction,
137
+ "image_file_name": image_file_name,
138
+ },
139
+ headers=self.headers,
140
+ )
141
+
142
+ def test_send_feedback_failure(self):
143
+ # πŸ’‘ NOTE Run test with: python -m unittest -vvv tests.test_image_classifier_app.TestMLService.test_send_feedback_failure
144
+ feedback = "This is a feedback"
145
+ score = 0.9346
146
+ prediction = "Eskimo_dog"
147
+ image_file_name = "dog.jpeg"
148
+ with mock.patch("requests.post") as mock_post:
149
+ mock_post.return_value.status_code = 500
150
+
151
+ response = ui_app.send_feedback(
152
+ self.token, feedback, score, prediction, image_file_name
153
+ )
154
+
155
+ self.assertEqual(response.status_code, 500)
156
+ mock_post.assert_called_once_with(
157
+ ui_app.API_BASE_URL + "/feedback",
158
+ json={
159
+ "feedback": feedback,
160
+ "score": score,
161
+ "predicted_class": prediction,
162
+ "image_file_name": image_file_name,
163
+ },
164
+ headers=self.headers,
165
+ )
166
+
167
+
168
+ if __name__ == "__main__":
169
+ unittest.main(verbosity=2)