Skip to content

Commit 8ef82ff

Browse files
CI/CD
1 parent 7b142cb commit 8ef82ff

File tree

6 files changed

+166
-89
lines changed

6 files changed

+166
-89
lines changed

.github/workflows/keploy.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Run Keploy API Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
keploy-tests:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout Repo
14+
uses: actions/checkout@v3
15+
16+
- name: Set up Python
17+
uses: actions/setup-python@v4
18+
with:
19+
python-version: '3.10'
20+
21+
- name: Install dependencies
22+
run: |
23+
python -m pip install --upgrade pip
24+
pip install -r requirements.txt
25+
26+
- name: Install Keploy
27+
run: |
28+
curl -s https://keploy.io/install.sh | bash
29+
30+
- name: Run Keploy tests
31+
run: |
32+
keploy test --delay 5

.gitignore

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Byte-compiled / cache
2+
__pycache__/
3+
*.py[cod]
4+
*.pyo
5+
6+
# Virtual environment
7+
.venv/
8+
env/
9+
venv/
10+
ENV/
11+
12+
# Environment variables
13+
.env
14+
15+
# Flask-specific
16+
instance/
17+
*.db
18+
19+
# Test coverage reports
20+
htmlcov/
21+
.coverage
22+
coverage.xml
23+
coverage_html/
24+
25+
# Logs
26+
*.log
27+
28+
# OS-specific files
29+
.DS_Store
30+
Thumbs.db
31+
32+
# Editor/IDE settings
33+
.vscode/
34+
.idea/
35+
*.swp
36+
*.swo

.idea/keployApi.iml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<head>
44
<meta charset="UTF-8">
55
<title>Task Manager</title>
6-
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
6+
<link rel="stylesheet" href="style.css">
77
</head>
88
<body>
99
<div class="container">
@@ -39,6 +39,6 @@ <h2>Tasks</h2>
3939
</div>
4040
</div>
4141

42-
<script src="{{ url_for('static', filename='script.js') }}"></script>
42+
<script src="./script.js"></script>
4343
</body>
4444
</html>

flask_api_server.py

Lines changed: 82 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,52 @@
11
from flask import Flask, request, jsonify, render_template
22
from flask_cors import CORS
3+
from flask_marshmallow import Marshmallow
4+
from flask_smorest import Api, Blueprint
5+
from flask.views import MethodView
36
from pymongo import MongoClient
47
from bson import ObjectId
58
from datetime import datetime
9+
from dotenv import load_dotenv
10+
from marshmallow import fields
611
import os
712
import logging
813

14+
# Load environment variables
15+
load_dotenv()
16+
917
# Logging for debugging
1018
logging.basicConfig(level=logging.DEBUG)
1119

12-
app = Flask(__name__)
20+
app = Flask(__name__, template_folder='docs')
1321
CORS(app)
1422

23+
# Swagger/OpenAPI config
24+
app.config["API_TITLE"] = "Task Manager API"
25+
app.config["API_VERSION"] = "v1"
26+
app.config["OPENAPI_VERSION"] = "3.0.3"
27+
app.config["OPENAPI_URL_PREFIX"] = "/"
28+
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
29+
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
30+
31+
api = Api(app)
32+
ma = Marshmallow(app)
33+
1534
# MongoDB connection
1635
try:
17-
client = MongoClient('mongodb://localhost:27017/')
36+
client = MongoClient(os.getenv("MONGO_URI", "mongodb://localhost:27017/"))
1837
db = client['task_manager']
1938
tasks_collection = db['tasks']
2039
users_collection = db['users']
2140
print("✅ Connected to MongoDB successfully!")
2241
except Exception as e:
2342
print(f"❌ Error connecting to MongoDB: {e}")
2443

25-
2644
# ---------- Utility Logic ----------
2745
def validate_task_data(data):
2846
if "title" not in data or not data["title"].strip():
2947
return False, "Title is required"
3048
return True, None
3149

32-
# Helper functions
3350
def serialize_doc(doc):
3451
if doc:
3552
doc['_id'] = str(doc['_id'])
@@ -39,106 +56,86 @@ def serialize_doc(doc):
3956
def serialize_docs(docs):
4057
return [serialize_doc(doc) for doc in docs]
4158

42-
# Route: Home (serve frontend)
4359
@app.route('/')
4460
def index():
4561
return render_template('index.html')
4662

47-
# -------------------- API ROUTES --------------------
48-
49-
# 1. GET all tasks
50-
@app.route('/api/tasks', methods=['GET'])
51-
def get_tasks():
52-
try:
63+
# Marshmallow schema
64+
class TaskSchema(ma.Schema):
65+
_id = fields.String(dump_only=True)
66+
title = fields.String(required=True)
67+
description = fields.String()
68+
status = fields.String()
69+
priority = fields.String()
70+
assigned_to = fields.String()
71+
due_date = fields.String()
72+
73+
class Meta:
74+
ordered = True
75+
fields = ("_id", "title", "description", "status", "priority", "assigned_to", "due_date")
76+
required = ("title",)
77+
78+
task_schema = TaskSchema()
79+
tasks_schema = TaskSchema(many=True)
80+
81+
# API Blueprint using Flask-Smorest
82+
blp = Blueprint("tasks", "tasks", url_prefix="/api/tasks", description="Operations on tasks")
83+
84+
@blp.route("/")
85+
class TaskListResource(MethodView):
86+
@blp.response(200, TaskSchema(many=True))
87+
def get(self):
5388
query = {}
5489
if request.args.get('status'):
5590
query['status'] = request.args.get('status')
5691
if request.args.get('priority'):
5792
query['priority'] = request.args.get('priority')
58-
5993
tasks = list(tasks_collection.find(query).sort('created_at', -1))
60-
return jsonify({
61-
'success': True,
62-
'data': serialize_docs(tasks),
63-
'count': len(tasks)
64-
}), 200
65-
except Exception as e:
66-
return jsonify({'success': False, 'error': str(e)}), 500
67-
68-
# 2. POST create a new task
69-
@app.route('/api/tasks', methods=['POST'])
70-
def create_task():
71-
try:
72-
data = request.get_json()
73-
if not data or not data.get('title'):
74-
return jsonify({'success': False, 'error': 'Title is required'}), 400
75-
76-
task = {
77-
'title': data['title'],
78-
'description': data.get('description', ''),
79-
'status': data.get('status', 'pending'),
80-
'priority': data.get('priority', 'medium'),
81-
'assigned_to': data.get('assigned_to', ''),
82-
'due_date': data.get('due_date'),
83-
'created_at': datetime.utcnow(),
84-
'updated_at': datetime.utcnow()
85-
}
86-
87-
result = tasks_collection.insert_one(task)
88-
task['_id'] = str(result.inserted_id)
89-
90-
return jsonify({
91-
'success': True,
92-
'message': 'Task created successfully',
93-
'data': serialize_doc(task)
94-
}), 201
95-
except Exception as e:
96-
return jsonify({'success': False, 'error': str(e)}), 500
97-
98-
# 3. PUT update a task
99-
@app.route('/api/tasks/<task_id>', methods=['PUT'])
100-
def update_task(task_id):
101-
if not ObjectId.is_valid(task_id):
102-
return jsonify({'success': False, 'error': 'Invalid task ID'}), 400
103-
try:
104-
data = request.get_json()
105-
update_fields = ['title', 'description', 'status', 'priority', 'assigned_to', 'due_date']
106-
update_data = {f: data[f] for f in update_fields if f in data}
94+
for task in tasks:
95+
task['_id'] = str(task['_id'])
96+
return tasks
97+
98+
@blp.arguments(TaskSchema)
99+
@blp.response(201, TaskSchema)
100+
def post(self, new_data):
101+
result = tasks_collection.insert_one(new_data)
102+
new_data['_id'] = str(result.inserted_id)
103+
return new_data
104+
105+
@blp.route("/<string:task_id>")
106+
class TaskResource(MethodView):
107+
@blp.response(200, TaskSchema)
108+
def get(self, task_id):
109+
if not ObjectId.is_valid(task_id):
110+
return jsonify({'success': False, 'error': 'Invalid task ID'}), 400
111+
task = tasks_collection.find_one({'_id': ObjectId(task_id)})
112+
if not task:
113+
return jsonify({'success': False, 'error': 'Task not found'}), 404
114+
task['_id'] = str(task['_id'])
115+
return task
116+
117+
@blp.arguments(TaskSchema)
118+
@blp.response(200, TaskSchema)
119+
def put(self, update_data, task_id):
120+
if not ObjectId.is_valid(task_id):
121+
return jsonify({'success': False, 'error': 'Invalid task ID'}), 400
107122
update_data['updated_at'] = datetime.utcnow()
108-
109123
result = tasks_collection.update_one({'_id': ObjectId(task_id)}, {'$set': update_data})
110124
if result.matched_count == 0:
111125
return jsonify({'success': False, 'error': 'Task not found'}), 404
126+
update_data['_id'] = task_id
127+
return update_data
112128

113-
updated_task = tasks_collection.find_one({'_id': ObjectId(task_id)})
114-
return jsonify({'success': True, 'data': serialize_doc(updated_task)}), 200
115-
except Exception as e:
116-
return jsonify({'success': False, 'error': str(e)}), 500
117-
118-
# 4. DELETE a task
119-
@app.route('/api/tasks/<task_id>', methods=['DELETE'])
120-
def delete_task(task_id):
121-
if not ObjectId.is_valid(task_id):
122-
return jsonify({'success': False, 'error': 'Invalid task ID'}), 400
123-
try:
129+
def delete(self, task_id):
130+
if not ObjectId.is_valid(task_id):
131+
return jsonify({'success': False, 'error': 'Invalid task ID'}), 400
124132
result = tasks_collection.delete_one({'_id': ObjectId(task_id)})
125133
if result.deleted_count == 0:
126134
return jsonify({'success': False, 'error': 'Task not found'}), 404
127-
return jsonify({'success': True, 'message': 'Task deleted successfully'}), 200
128-
except Exception as e:
129-
return jsonify({'success': False, 'error': str(e)}), 500
135+
return jsonify({'success': True, 'message': 'Task deleted successfully'})
136+
137+
api.register_blueprint(blp)
130138

131-
# 5. GET a single task
132-
@app.route('/api/tasks/<task_id>', methods=['GET'])
133-
def get_task(task_id):
134-
if not ObjectId.is_valid(task_id):
135-
return jsonify({'success': False, 'error': 'Invalid task ID'}), 400
136-
task = tasks_collection.find_one({'_id': ObjectId(task_id)})
137-
if not task:
138-
return jsonify({'success': False, 'error': 'Task not found'}), 404
139-
return jsonify({'success': True, 'data': serialize_doc(task)}), 200
140-
141-
# 6. GET task stats
142139
@app.route('/api/stats', methods=['GET'])
143140
def get_stats():
144141
try:
@@ -154,7 +151,5 @@ def get_stats():
154151
except Exception as e:
155152
return jsonify({'success': False, 'error': str(e)}), 500
156153

157-
# -------------------- END --------------------
158-
159154
if __name__ == '__main__':
160155
app.run(debug=True, port=5000)

schemas.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from flask_marshmallow import Marshmallow
2+
from marshmallow import fields
3+
from flask_api_server import app
4+
5+
ma = Marshmallow(app)
6+
7+
class TaskSchema(ma.Schema):
8+
class Meta:
9+
fields = ("_id", "title", "description", "status", "priority", "assigned_to", "due_date")
10+
11+
task_schema = TaskSchema()
12+
tasks_schema = TaskSchema(many=True)

0 commit comments

Comments
 (0)