From 690b2d9be07df703e19e3f2579d487bd86830474 Mon Sep 17 00:00:00 2001 From: Atul Rajput Date: Thu, 19 Jun 2025 09:59:14 +0530 Subject: [PATCH 1/4] Add Flask, SQLite Sample App Signed-off-by: Atul Rajput --- flask-sqlite/.coverage.keploy | Bin 0 -> 53248 bytes flask-sqlite/.coveragerc | 4 + flask-sqlite/.gitignore | 35 +++++ flask-sqlite/README.md | 126 ++++++++++++++++++ flask-sqlite/app.py | 50 +++++++ flask-sqlite/keploy.yml | 67 ++++++++++ flask-sqlite/keploy/.gitignore | 2 + .../keploy/test-set-0/tests/test-1.yaml | 61 +++++++++ .../keploy/test-set-0/tests/test-2.yaml | 61 +++++++++ .../keploy/test-set-0/tests/test-3.yaml | 47 +++++++ .../keploy/test-set-0/tests/test-4.yaml | 47 +++++++ .../keploy/test-set-0/tests/test-5.yaml | 72 ++++++++++ .../keploy/test-set-0/tests/test-6.yaml | 61 +++++++++ flask-sqlite/models.py | 8 ++ flask-sqlite/requirements.txt | 2 + 15 files changed, 643 insertions(+) create mode 100644 flask-sqlite/.coverage.keploy create mode 100644 flask-sqlite/.coveragerc create mode 100644 flask-sqlite/.gitignore create mode 100644 flask-sqlite/README.md create mode 100644 flask-sqlite/app.py create mode 100755 flask-sqlite/keploy.yml create mode 100644 flask-sqlite/keploy/.gitignore create mode 100755 flask-sqlite/keploy/test-set-0/tests/test-1.yaml create mode 100755 flask-sqlite/keploy/test-set-0/tests/test-2.yaml create mode 100755 flask-sqlite/keploy/test-set-0/tests/test-3.yaml create mode 100755 flask-sqlite/keploy/test-set-0/tests/test-4.yaml create mode 100755 flask-sqlite/keploy/test-set-0/tests/test-5.yaml create mode 100755 flask-sqlite/keploy/test-set-0/tests/test-6.yaml create mode 100644 flask-sqlite/models.py create mode 100644 flask-sqlite/requirements.txt diff --git a/flask-sqlite/.coverage.keploy b/flask-sqlite/.coverage.keploy new file mode 100644 index 0000000000000000000000000000000000000000..cf259d2efa445902bac4d9a805eef8e90b7a4e2d GIT binary patch literal 53248 zcmeI)%WoS+90%}S+qG-Q))rNj6{0G0fi!9oW1$L_9w5*hQY!T+gj9q(UXSB#y}Rx{ z(&T`^Em8#%{{TW9;oqQt0)GM*RQ1Avaw-RYznxt_+PLbWQdQ@>@?#$}vooK0dHuR} z<)RZQ(e(pYM&fd6$3hr}QK5T{?%7hJl|r^b&*sGXtksIKvHN|^`rN1%e>AMG zYL~3_>d&=z7yhWaCubR4PCy(i+lYM8cf6#SUg$)Q?+LZ1>^M@L0}HsuXxTz|lck}wZgPQc>X_-dCUnM; z4~UrtPa}6JbDp2EjwAA^DRPo$FLv8Rek*ndTsag&pE&Yo#hev4^3%e;bln_Km*etI zRcA)dHL7F9o^vNwqsm_sM*7B^TGim~@=4iDtTr8IAOH1axa?*^_+De~FQ%xwV z$pePb?2xNu7zf!v@}fa9i!mN0vp$j@q2jEUiuKKF<8$4f&+FW1(t0;j*|Avn@RKJxo4bToxpfI zv!_t2NpR}km;EqwXjhxzN&>9YPsE3PZOPC8%lG^|slv7b99pGOZO6j7{Drwpl!K9gpa zXnK@KtStSF0hw^BuI%XU&XqEe;@zHlE;&6LotKe{99Ibrl4K@nSa)S8>_E{$noCz# z@-J!zTwARsKk`-bQGO{mTsBMf)2Gd0oOVV%&a^_B)LofnJKfApHIiozj@!#68gS{= z&SX1by9o~7)MwzqP1j&sD`cV4*E*F|zQ8xFrv}}yKmY;|fB*y_009U<00Izz00bZ~ ze*$?kXO{T-KWF`ESbx(476?E90uX=z1Rwwb2tWV=5P$##-b{gNE`QR>zT)woW#*Ua z>6ZXL*f_h<tVf3R$oluqEFf|X0SG_<0uX=z1Rwwb2tWV=5P(1?P|cq-vv&b< zmHbjUeG@=`{=aQlzgpW_SUf`j0uX=z1Rwwb2tWV=5P$##ATUGVMy_BC&oz3!s~R$j z`+>YQh@-}N72b~gL1Xi3<8t8NQg#$JE)D1}2Cn&WU@OrGr90@WaDA{J^?a|(Bo$7}jsrg&8HoZU{gC0uX=z1Rwwb2tWV= z5P$##Mgm_JhUd&zdx<`}{?C0;7@jG=+QjwsKmPx3RDoE500bZa0SG_<0uX=z1Rwwb z2+X5E-pm^{{`}vx{xj%?1p*L&00bZa0SG_<0uX=z1Rwx`ITXm}Yi0fW|Hp>)kM(#C zi-;mY00Izz00bZa0SG_<0uX=z1P+-%DOWJd$8wL&1+!2tzx~%w4}bsmVP60K|A}Ee zwVoU@5EK^z5P$##AOHafKmY;|fB*y_FxLV_dLzKh{*UYbL*EPl009U<00Izz00bZa0SG_<0&_0FzyHVe|D0Dc ZiU$D*KmY;|fB*y_009U<00M_z;C}_=wF&?L literal 0 HcmV?d00001 diff --git a/flask-sqlite/.coveragerc b/flask-sqlite/.coveragerc new file mode 100644 index 0000000..152922e --- /dev/null +++ b/flask-sqlite/.coveragerc @@ -0,0 +1,4 @@ +[run] +omit = + /usr/* +sigterm = true diff --git a/flask-sqlite/.gitignore b/flask-sqlite/.gitignore new file mode 100644 index 0000000..eff16dd --- /dev/null +++ b/flask-sqlite/.gitignore @@ -0,0 +1,35 @@ +# Python bytecode +__pycache__/ +*.py[cod] +*.pyo + +# SQLite DB file (do not commit local dev DB) +*.db +instance/ + +# Keploy reports (tests are OK, reports are not needed) +keploy/reports/ + +# Coverage reports +coverage.* +*.cover +.hypothesis/ + +# Keploy traces/cache +*.log +keploy/test-set-*/coverage.* +keploy/test-set-*/mocks/ + +# Environment files +.env +.venv/ +env/ +venv/ + +# IDE files +.vscode/ +.idea/ + +# macOS/Linux system files +.DS_Store +Thumbs.db diff --git a/flask-sqlite/README.md b/flask-sqlite/README.md new file mode 100644 index 0000000..8532cad --- /dev/null +++ b/flask-sqlite/README.md @@ -0,0 +1,126 @@ +# Flask + SQLite Sample App with Keploy Integration + +This is a simple **Student Management REST API** built using Python's Flask framework and SQLite for storage. It demonstrates basic **CRUD operations** (Create, Read, Update, Delete) and showcases how to write **API tests with [Keploy](https://keploy.io)** by auto-capturing HTTP calls as test cases. + +--- + +## 🚀 Features + +- 🧑 Add, retrieve, update, and delete students +- 💾 Uses SQLite — no external DB setup required +- 🔌 RESTful API with JSON input/output +- ✅ Auto-generate test cases using [Keploy CLI](https://docs.keploy.io) +- 🔄 Replay & validate API responses with Keploy + +--- + +## 📦 Tech Stack + +- **Flask** — Lightweight web framework +- **Flask-SQLAlchemy** — ORM for SQLite +- **SQLite** — Built-in relational DB +- **Keploy** — Testing toolkit for API auto-mocking and regression testing + +--- + +## 🛠 Installation + +1. **Clone the repository** +```bash +git clone https://github.com//samples-python.git +cd samples-python/flask-sqlite +``` + +2. Set up virtual environment (optional) +```bash +python3 -m venv venv +source venv/bin/activate +``` + +3. Install Dependencies + +```bash +pip install -r requirements.txt +``` +4. Run the Flask app + +```bash +python app.py +``` +--- + +## API Endpoints + +```bash + +| Method | Endpoint | Description | +| ------ | ---------------- | -------------------- | +| GET | `/students` | Get all students | +| POST | `/students` | Add a new student | +| PUT | `/students/` | Update student by ID | +| DELETE | `/students/` | Delete student by ID | + +``` +--- + +## Sample Curl Commands + +### Add a student + +```bash +curl -X POST http://localhost:5000/students \ + -H "Content-Type: application/json" \ + -d '{"name": "Alice", "age": 21}' + +# Get all students +curl http://localhost:5000/students + +# Update student +curl -X PUT http://localhost:5000/students/1 \ + -H "Content-Type: application/json" \ + -d '{"name": "Alice Updated", "age": 22}' + +# Delete student +curl -X DELETE http://localhost:5000/students/1 + +``` +--- + +## Running Keploy Tests + +Step 1: Record Tests +Start Keploy in record mode: + +```bash +keploy record --command "python app.py" +``` +Send some API requests via curl or Postman to generate test cases. + +Step 2: Replay Tests +After recording, stop the app and run: + +```bash +keploy test --command "python app.py" +``` +> Keploy will replay the previously captured requests and validate responses. + +## Folder Structure + +```bash +flask-sqlite/ +├── app.py # Flask app entry point +├── models.py # Student model +├── requirements.txt # Python dependencies +├── keploy.yml # Keploy config file +├── keploy/ # Auto-generated test cases +└── README.md # You are here! +``` +--- + +## Contributing +> Want to improve or add another example (e.g. FastAPI, SQLModel, etc.)? Contributions are welcome! Fork this repo, create your example folder, and submit a PR. + +## About Keploy +Keploy is a developer-friendly open-source testing toolkit that auto-generates test cases from API calls in real-time and replays them to catch regressions — without writing any test code. + +> Built with ❤️ for the Open Source Community. \ No newline at end of file diff --git a/flask-sqlite/app.py b/flask-sqlite/app.py new file mode 100644 index 0000000..4a5c33b --- /dev/null +++ b/flask-sqlite/app.py @@ -0,0 +1,50 @@ +from flask import Flask, request, jsonify +from models import db, Student + + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///students.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +db.init_app(app) + +@app.route('/') +def home(): + return jsonify({"message": "Flask Student API"}), 200 + +@app.route('/students', methods=['GET']) +def get_students(): + students = Student.query.all() + return jsonify([ + {"id": s.id, "name": s.name, "age": s.age} for s in students + ]) + +@app.route('/students', methods=['POST']) +def add_student(): + data = request.get_json() + student = Student(name=data['name'], age=data['age']) + db.session.add(student) + db.session.commit() + return jsonify({"id": student.id}), 201 + +@app.route('/students/', methods=['PUT']) +def update_student(id): + student = Student.query.get_or_404(id) + data = request.get_json() + student.name = data['name'] + student.age = data['age'] + db.session.commit() + return jsonify({"message": "Updated"}), 200 + +@app.route('/students/', methods=['DELETE']) +def delete_student(id): + student = Student.query.get_or_404(id) + db.session.delete(student) + db.session.commit() + return jsonify({"message": "Deleted"}), 200 + +if __name__ == '__main__': + with app.app_context(): + db.create_all() + app.run(debug=True) + diff --git a/flask-sqlite/keploy.yml b/flask-sqlite/keploy.yml new file mode 100755 index 0000000..aa33808 --- /dev/null +++ b/flask-sqlite/keploy.yml @@ -0,0 +1,67 @@ +# Generated by Keploy (2.6.11) +path: "" +appId: 0 +appName: flask-sqlite +command: .venv/bin/python app.py +templatize: + testSets: [] +port: 0 +e2e: false +dnsPort: 26789 +proxyPort: 16789 +debug: false +disableTele: false +disableANSI: false +containerName: "" +networkName: "" +buildDelay: 30 +test: + selectedTests: {} + globalNoise: + global: {} + test-sets: {} + delay: 5 + host: "" + port: 0 + apiTimeout: 5 + skipCoverage: false + coverageReportPath: "" + ignoreOrdering: true + mongoPassword: default@123 + language: "" + removeUnusedMocks: false + fallBackOnMiss: false + jacocoAgentPath: "" + basePath: "" + mocking: true + ignoredTests: {} + disableLineCoverage: false + disableMockUpload: true + useLocalMock: false + updateTemplate: false + mustPass: false + maxFailAttempts: 5 + maxFlakyChecks: 1 +record: + filters: [] + basePath: "" + recordTimer: 0s +configPath: "" +bypassRules: [] +generateGithubActions: false +keployContainer: keploy-v2 +keployNetwork: keploy-network +cmdType: native +contract: + services: [] + tests: [] + path: "" + download: false + generate: false + driven: consumer + mappings: + servicesMapping: {} + self: s1 +inCi: false + +# Visit [https://keploy.io/docs/running-keploy/configuration-file/] to learn about using keploy through configration file. diff --git a/flask-sqlite/keploy/.gitignore b/flask-sqlite/keploy/.gitignore new file mode 100644 index 0000000..5137843 --- /dev/null +++ b/flask-sqlite/keploy/.gitignore @@ -0,0 +1,2 @@ + +/reports/ diff --git a/flask-sqlite/keploy/test-set-0/tests/test-1.yaml b/flask-sqlite/keploy/test-set-0/tests/test-1.yaml new file mode 100755 index 0000000..5e3e160 --- /dev/null +++ b/flask-sqlite/keploy/test-set-0/tests/test-1.yaml @@ -0,0 +1,61 @@ +# Generated by Keploy (2.6.11) +version: api.keploy.io/v1beta1 +kind: Http +name: test-1 +spec: + metadata: {} + req: + method: GET + proto_major: 1 + proto_minor: 1 + url: http://localhost:5000/students + header: + Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: gzip, deflate, br, zstd + Accept-Language: en-US,en;q=0.5 + Connection: keep-alive + Cookie: _ga_9C3YK4FNWZ=GS2.1.s1746835762$o1$g1$t1746835798$j0$l0$h0; _ga=GA1.1.370021656.1746835763; _clck=1e6823p%7C2%7Cfww%7C0%7C1996; __hstc=181257784.e3804237d83ad93b9ef5fdbdd48354d1.1750295949745.1750295949745.1750295949745.1; hubspotutk=e3804237d83ad93b9ef5fdbdd48354d1; __hssrc=1; _clsk=dk2mvr%7C1750295950556%7C1%7C1%7Cl.clarity.ms%2Fcollect; messagesUtk=a1a71bf707fd412b8e4bf788336568d9 + Host: localhost:5000 + Priority: u=0, i + Sec-Fetch-Dest: document + Sec-Fetch-Mode: navigate + Sec-Fetch-Site: none + Sec-Fetch-User: ?1 + Upgrade-Insecure-Requests: "1" + User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0 + body: "" + timestamp: 2025-06-19T09:36:00.825424089+05:30 + resp: + status_code: 200 + header: + Content-Length: "3" + Content-Type: application/json + Date: Thu, 19 Jun 2025 04:06:00 GMT + Server: Werkzeug/3.1.3 Python/3.12.3 + body: | + [] + status_message: OK + proto_major: 0 + proto_minor: 0 + timestamp: 2025-06-19T09:36:02.898108619+05:30 + objects: [] + assertions: + noise: + header.Date: [] + created: 1750305962 +curl: | + curl --request GET \ + --url http://localhost:5000/students \ + --header 'Sec-Fetch-User: ?1' \ + --header 'Cookie: _ga_9C3YK4FNWZ=GS2.1.s1746835762$o1$g1$t1746835798$j0$l0$h0; _ga=GA1.1.370021656.1746835763; _clck=1e6823p%7C2%7Cfww%7C0%7C1996; __hstc=181257784.e3804237d83ad93b9ef5fdbdd48354d1.1750295949745.1750295949745.1750295949745.1; hubspotutk=e3804237d83ad93b9ef5fdbdd48354d1; __hssrc=1; _clsk=dk2mvr%7C1750295950556%7C1%7C1%7Cl.clarity.ms%2Fcollect; messagesUtk=a1a71bf707fd412b8e4bf788336568d9' \ + --header 'Accept-Language: en-US,en;q=0.5' \ + --header 'Sec-Fetch-Dest: document' \ + --header 'Sec-Fetch-Mode: navigate' \ + --header 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \ + --header 'Priority: u=0, i' \ + --header 'Accept-Encoding: gzip, deflate, br, zstd' \ + --header 'User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0' \ + --header 'Upgrade-Insecure-Requests: 1' \ + --header 'Sec-Fetch-Site: none' \ + --header 'Host: localhost:5000' \ + --header 'Connection: keep-alive' \ diff --git a/flask-sqlite/keploy/test-set-0/tests/test-2.yaml b/flask-sqlite/keploy/test-set-0/tests/test-2.yaml new file mode 100755 index 0000000..adc5362 --- /dev/null +++ b/flask-sqlite/keploy/test-set-0/tests/test-2.yaml @@ -0,0 +1,61 @@ +# Generated by Keploy (2.6.11) +version: api.keploy.io/v1beta1 +kind: Http +name: test-2 +spec: + metadata: {} + req: + method: GET + proto_major: 1 + proto_minor: 1 + url: http://localhost:5000/favicon.ico + header: + Accept: image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5 + Accept-Encoding: gzip, deflate, br, zstd + Accept-Language: en-US,en;q=0.5 + Connection: keep-alive + Host: localhost:5000 + Priority: u=6 + Referer: http://localhost:5000/students + Sec-Fetch-Dest: image + Sec-Fetch-Mode: no-cors + Sec-Fetch-Site: cross-site + User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0 + body: "" + timestamp: 2025-06-19T09:36:00.905501417+05:30 + resp: + status_code: 404 + header: + Content-Length: "207" + Content-Type: text/html; charset=utf-8 + Date: Thu, 19 Jun 2025 04:06:00 GMT + Server: Werkzeug/3.1.3 Python/3.12.3 + body: | + + + 404 Not Found +

Not Found

+

The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.

+ status_message: Not Found + proto_major: 0 + proto_minor: 0 + timestamp: 2025-06-19T09:36:02.999325414+05:30 + objects: [] + assertions: + noise: + header.Date: [] + created: 1750305962 +curl: | + curl --request GET \ + --url http://localhost:5000/favicon.ico \ + --header 'Accept: image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5' \ + --header 'User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0' \ + --header 'Sec-Fetch-Mode: no-cors' \ + --header 'Host: localhost:5000' \ + --header 'Connection: keep-alive' \ + --header 'Referer: http://localhost:5000/students' \ + --header 'Sec-Fetch-Site: cross-site' \ + --header 'Sec-Fetch-Dest: image' \ + --header 'Accept-Language: en-US,en;q=0.5' \ + --header 'Priority: u=6' \ + --header 'Accept-Encoding: gzip, deflate, br, zstd' \ diff --git a/flask-sqlite/keploy/test-set-0/tests/test-3.yaml b/flask-sqlite/keploy/test-set-0/tests/test-3.yaml new file mode 100755 index 0000000..be4b920 --- /dev/null +++ b/flask-sqlite/keploy/test-set-0/tests/test-3.yaml @@ -0,0 +1,47 @@ +# Generated by Keploy (2.6.11) +version: api.keploy.io/v1beta1 +kind: Http +name: test-3 +spec: + metadata: {} + req: + method: POST + proto_major: 1 + proto_minor: 1 + url: http://localhost:5000/students + header: + Accept: '*/*' + Content-Length: "31" + Content-Type: application/json + Host: localhost:5000 + User-Agent: curl/8.5.0 + body: '{"name": "TestUser", "age": 20}' + timestamp: 2025-06-19T09:36:20.274605472+05:30 + resp: + status_code: 201 + header: + Content-Length: "14" + Content-Type: application/json + Date: Thu, 19 Jun 2025 04:06:20 GMT + Server: Werkzeug/3.1.3 Python/3.12.3 + body: | + { + "id": 1 + } + status_message: Created + proto_major: 0 + proto_minor: 0 + timestamp: 2025-06-19T09:36:22.355090014+05:30 + objects: [] + assertions: + noise: + header.Date: [] + created: 1750305982 +curl: |- + curl --request POST \ + --url http://localhost:5000/students \ + --header 'User-Agent: curl/8.5.0' \ + --header 'Accept: */*' \ + --header 'Content-Type: application/json' \ + --header 'Host: localhost:5000' \ + --data "{\"name\": \"TestUser\", \"age\": 20}" diff --git a/flask-sqlite/keploy/test-set-0/tests/test-4.yaml b/flask-sqlite/keploy/test-set-0/tests/test-4.yaml new file mode 100755 index 0000000..67f5090 --- /dev/null +++ b/flask-sqlite/keploy/test-set-0/tests/test-4.yaml @@ -0,0 +1,47 @@ +# Generated by Keploy (2.6.11) +version: api.keploy.io/v1beta1 +kind: Http +name: test-4 +spec: + metadata: {} + req: + method: POST + proto_major: 1 + proto_minor: 1 + url: http://localhost:5000/students + header: + Accept: '*/*' + Content-Length: "34" + Content-Type: application/json + Host: localhost:5000 + User-Agent: curl/8.5.0 + body: '{"name": "TestStudent", "age": 22}' + timestamp: 2025-06-19T09:36:23.422507862+05:30 + resp: + status_code: 201 + header: + Content-Length: "14" + Content-Type: application/json + Date: Thu, 19 Jun 2025 04:06:23 GMT + Server: Werkzeug/3.1.3 Python/3.12.3 + body: | + { + "id": 2 + } + status_message: Created + proto_major: 0 + proto_minor: 0 + timestamp: 2025-06-19T09:36:25.466032323+05:30 + objects: [] + assertions: + noise: + header.Date: [] + created: 1750305985 +curl: |- + curl --request POST \ + --url http://localhost:5000/students \ + --header 'Content-Type: application/json' \ + --header 'Host: localhost:5000' \ + --header 'User-Agent: curl/8.5.0' \ + --header 'Accept: */*' \ + --data "{\"name\": \"TestStudent\", \"age\": 22}" diff --git a/flask-sqlite/keploy/test-set-0/tests/test-5.yaml b/flask-sqlite/keploy/test-set-0/tests/test-5.yaml new file mode 100755 index 0000000..e47d732 --- /dev/null +++ b/flask-sqlite/keploy/test-set-0/tests/test-5.yaml @@ -0,0 +1,72 @@ +# Generated by Keploy (2.6.11) +version: api.keploy.io/v1beta1 +kind: Http +name: test-5 +spec: + metadata: {} + req: + method: GET + proto_major: 1 + proto_minor: 1 + url: http://localhost:5000/students + header: + Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 + Accept-Encoding: gzip, deflate, br, zstd + Accept-Language: en-US,en;q=0.5 + Connection: keep-alive + Cookie: _ga_9C3YK4FNWZ=GS2.1.s1746835762$o1$g1$t1746835798$j0$l0$h0; _ga=GA1.1.370021656.1746835763; _clck=1e6823p%7C2%7Cfww%7C0%7C1996; __hstc=181257784.e3804237d83ad93b9ef5fdbdd48354d1.1750295949745.1750295949745.1750295949745.1; hubspotutk=e3804237d83ad93b9ef5fdbdd48354d1; __hssrc=1; _clsk=dk2mvr%7C1750295950556%7C1%7C1%7Cl.clarity.ms%2Fcollect; messagesUtk=a1a71bf707fd412b8e4bf788336568d9 + Host: localhost:5000 + Priority: u=0, i + Sec-Fetch-Dest: document + Sec-Fetch-Mode: navigate + Sec-Fetch-Site: none + Sec-Fetch-User: ?1 + Upgrade-Insecure-Requests: "1" + User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0 + body: "" + timestamp: 2025-06-19T09:36:26.714533+05:30 + resp: + status_code: 200 + header: + Content-Length: "126" + Content-Type: application/json + Date: Thu, 19 Jun 2025 04:06:26 GMT + Server: Werkzeug/3.1.3 Python/3.12.3 + body: | + [ + { + "age": 20, + "id": 1, + "name": "TestUser" + }, + { + "age": 22, + "id": 2, + "name": "TestStudent" + } + ] + status_message: OK + proto_major: 0 + proto_minor: 0 + timestamp: 2025-06-19T09:36:28.780097142+05:30 + objects: [] + assertions: + noise: + header.Date: [] + created: 1750305988 +curl: | + curl --request GET \ + --url http://localhost:5000/students \ + --header 'Cookie: _ga_9C3YK4FNWZ=GS2.1.s1746835762$o1$g1$t1746835798$j0$l0$h0; _ga=GA1.1.370021656.1746835763; _clck=1e6823p%7C2%7Cfww%7C0%7C1996; __hstc=181257784.e3804237d83ad93b9ef5fdbdd48354d1.1750295949745.1750295949745.1750295949745.1; hubspotutk=e3804237d83ad93b9ef5fdbdd48354d1; __hssrc=1; _clsk=dk2mvr%7C1750295950556%7C1%7C1%7Cl.clarity.ms%2Fcollect; messagesUtk=a1a71bf707fd412b8e4bf788336568d9' \ + --header 'Sec-Fetch-Dest: document' \ + --header 'Accept-Encoding: gzip, deflate, br, zstd' \ + --header 'Sec-Fetch-Mode: navigate' \ + --header 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \ + --header 'Host: localhost:5000' \ + --header 'Connection: keep-alive' \ + --header 'Accept-Language: en-US,en;q=0.5' \ + --header 'Upgrade-Insecure-Requests: 1' \ + --header 'Sec-Fetch-Site: none' \ + --header 'Sec-Fetch-User: ?1' \ + --header 'Priority: u=0, i' \ + --header 'User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0' \ diff --git a/flask-sqlite/keploy/test-set-0/tests/test-6.yaml b/flask-sqlite/keploy/test-set-0/tests/test-6.yaml new file mode 100755 index 0000000..ebb18a5 --- /dev/null +++ b/flask-sqlite/keploy/test-set-0/tests/test-6.yaml @@ -0,0 +1,61 @@ +# Generated by Keploy (2.6.11) +version: api.keploy.io/v1beta1 +kind: Http +name: test-6 +spec: + metadata: {} + req: + method: GET + proto_major: 1 + proto_minor: 1 + url: http://localhost:5000/favicon.ico + header: + Accept: image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5 + Accept-Encoding: gzip, deflate, br, zstd + Accept-Language: en-US,en;q=0.5 + Connection: keep-alive + Host: localhost:5000 + Priority: u=6 + Referer: http://localhost:5000/students + Sec-Fetch-Dest: image + Sec-Fetch-Mode: no-cors + Sec-Fetch-Site: cross-site + User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0 + body: "" + timestamp: 2025-06-19T09:36:26.773273264+05:30 + resp: + status_code: 404 + header: + Content-Length: "207" + Content-Type: text/html; charset=utf-8 + Date: Thu, 19 Jun 2025 04:06:26 GMT + Server: Werkzeug/3.1.3 Python/3.12.3 + body: | + + + 404 Not Found +

Not Found

+

The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.

+ status_message: Not Found + proto_major: 0 + proto_minor: 0 + timestamp: 2025-06-19T09:36:28.780258492+05:30 + objects: [] + assertions: + noise: + header.Date: [] + created: 1750305988 +curl: | + curl --request GET \ + --url http://localhost:5000/favicon.ico \ + --header 'Host: localhost:5000' \ + --header 'Accept-Language: en-US,en;q=0.5' \ + --header 'Connection: keep-alive' \ + --header 'Referer: http://localhost:5000/students' \ + --header 'User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0' \ + --header 'Accept-Encoding: gzip, deflate, br, zstd' \ + --header 'Accept: image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5' \ + --header 'Sec-Fetch-Site: cross-site' \ + --header 'Priority: u=6' \ + --header 'Sec-Fetch-Dest: image' \ + --header 'Sec-Fetch-Mode: no-cors' \ diff --git a/flask-sqlite/models.py b/flask-sqlite/models.py new file mode 100644 index 0000000..99e387e --- /dev/null +++ b/flask-sqlite/models.py @@ -0,0 +1,8 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + +class Student(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + age = db.Column(db.Integer, nullable=False) diff --git a/flask-sqlite/requirements.txt b/flask-sqlite/requirements.txt new file mode 100644 index 0000000..41fbe74 --- /dev/null +++ b/flask-sqlite/requirements.txt @@ -0,0 +1,2 @@ +Flask==3.0.2 +Flask_SQLAlchemy==3.1.1 From 99dae2a5276931ae818ac4e07643cf373ce5ffbf Mon Sep 17 00:00:00 2001 From: Atul Rajput Date: Thu, 19 Jun 2025 11:21:34 +0530 Subject: [PATCH 2/4] Add Validation check Signed-off-by: Atul Rajput --- flask-sqlite/app.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/flask-sqlite/app.py b/flask-sqlite/app.py index 4a5c33b..cd561a9 100644 --- a/flask-sqlite/app.py +++ b/flask-sqlite/app.py @@ -1,7 +1,6 @@ from flask import Flask, request, jsonify from models import db, Student - app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///students.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False @@ -22,7 +21,16 @@ def get_students(): @app.route('/students', methods=['POST']) def add_student(): data = request.get_json() - student = Student(name=data['name'], age=data['age']) + + # Validation + if not data: + return jsonify({"error": "Missing JSON body"}), 400 + if 'name' not in data or not isinstance(data['name'], str) or not data['name'].strip(): + return jsonify({"error": "Invalid or missing 'name'"}), 400 + if 'age' not in data or not isinstance(data['age'], int): + return jsonify({"error": "Invalid or missing 'age'"}), 400 + + student = Student(name=data['name'].strip(), age=data['age']) db.session.add(student) db.session.commit() return jsonify({"id": student.id}), 201 @@ -31,8 +39,19 @@ def add_student(): def update_student(id): student = Student.query.get_or_404(id) data = request.get_json() - student.name = data['name'] - student.age = data['age'] + + # Validation + if not data: + return jsonify({"error": "Missing JSON body"}), 400 + if 'name' in data: + if not isinstance(data['name'], str) or not data['name'].strip(): + return jsonify({"error": "Invalid 'name'"}), 400 + student.name = data['name'].strip() + if 'age' in data: + if not isinstance(data['age'], int): + return jsonify({"error": "Invalid 'age'"}), 400 + student.age = data['age'] + db.session.commit() return jsonify({"message": "Updated"}), 200 @@ -47,4 +66,3 @@ def delete_student(id): with app.app_context(): db.create_all() app.run(debug=True) - From 6f7e6a624055fe573a7adab06d116937ac53dc7a Mon Sep 17 00:00:00 2001 From: Atul Rajput Date: Thu, 19 Jun 2025 11:31:49 +0530 Subject: [PATCH 3/4] Refactor Validation Signed-off-by: Atul Rajput --- flask-sqlite/app.py | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/flask-sqlite/app.py b/flask-sqlite/app.py index cd561a9..b66f92f 100644 --- a/flask-sqlite/app.py +++ b/flask-sqlite/app.py @@ -7,6 +7,27 @@ db.init_app(app) +# 🔧 Validation Helper Function +def validate_student_data(data, partial=False): + if not data: + return "Missing JSON body" + + if not partial: + if 'name' not in data: + return "Missing 'name'" + if 'age' not in data: + return "Missing 'age'" + + if 'name' in data: + if not isinstance(data['name'], str) or not data['name'].strip(): + return "Invalid 'name'" + + if 'age' in data: + if not isinstance(data['age'], int): + return "Invalid 'age'" + + return None # No error + @app.route('/') def home(): return jsonify({"message": "Flask Student API"}), 200 @@ -21,14 +42,9 @@ def get_students(): @app.route('/students', methods=['POST']) def add_student(): data = request.get_json() - - # Validation - if not data: - return jsonify({"error": "Missing JSON body"}), 400 - if 'name' not in data or not isinstance(data['name'], str) or not data['name'].strip(): - return jsonify({"error": "Invalid or missing 'name'"}), 400 - if 'age' not in data or not isinstance(data['age'], int): - return jsonify({"error": "Invalid or missing 'age'"}), 400 + error = validate_student_data(data) + if error: + return jsonify({"error": error}), 400 student = Student(name=data['name'].strip(), age=data['age']) db.session.add(student) @@ -39,17 +55,13 @@ def add_student(): def update_student(id): student = Student.query.get_or_404(id) data = request.get_json() + error = validate_student_data(data, partial=True) + if error: + return jsonify({"error": error}), 400 - # Validation - if not data: - return jsonify({"error": "Missing JSON body"}), 400 if 'name' in data: - if not isinstance(data['name'], str) or not data['name'].strip(): - return jsonify({"error": "Invalid 'name'"}), 400 student.name = data['name'].strip() if 'age' in data: - if not isinstance(data['age'], int): - return jsonify({"error": "Invalid 'age'"}), 400 student.age = data['age'] db.session.commit() From 53c5e17265744986a4c1b2436f268dbeae8e0169 Mon Sep 17 00:00:00 2001 From: Atul Rajput Date: Thu, 6 Nov 2025 04:59:22 +0530 Subject: [PATCH 4/4] feat: Enhance student management API with validation, pagination, and error handling --- flask-sqlite/README.md | 36 +++++++++- flask-sqlite/app.py | 130 ++++++++++++++++++++++++++-------- flask-sqlite/requirements.txt | 5 +- 3 files changed, 137 insertions(+), 34 deletions(-) diff --git a/flask-sqlite/README.md b/flask-sqlite/README.md index 8532cad..9b00223 100644 --- a/flask-sqlite/README.md +++ b/flask-sqlite/README.md @@ -65,16 +65,26 @@ python app.py ## Sample Curl Commands +http://localhost:5000/ +```json +{ + "message": "Flask Student API" +} +``` + + ### Add a student ```bash curl -X POST http://localhost:5000/students \ -H "Content-Type: application/json" \ -d '{"name": "Alice", "age": 21}' - +``` # Get all students curl http://localhost:5000/students + + # Update student curl -X PUT http://localhost:5000/students/1 \ -H "Content-Type: application/json" \ @@ -123,4 +133,26 @@ flask-sqlite/ ## About Keploy Keploy is a developer-friendly open-source testing toolkit that auto-generates test cases from API calls in real-time and replays them to catch regressions — without writing any test code. -> Built with ❤️ for the Open Source Community. \ No newline at end of file +> Built with ❤️ for the Open Source Community. + +| Stage | Improvement Type | Description | +| ------- | ---------------------------- | ------------------------------------------- | +| Stage 1 | Validation & Pagination | Add search, filter, pagination for students | +| Stage 2 | Security | Add authentication, role-based access | +| Stage 3 | Code Quality | Integrate Marshmallow/Pydantic, add tests | +| Stage 4 | Documentation & Deployment | Add Swagger, Dockerize, CI/CD pipeline | +| Stage 5 | Frontend & Realtime Features | Build React UI, add WebSocket updates | + +--- + +#### Stage 1 + +Get End points + +/students?page=1&per_page=5 — Gets first 5 students. + +/students?name=John — Filters students whose names contain "John". + +/students?age=20 — Filters students aged 20. + +/students?page=2&per_page=3&name=ann — Combination. diff --git a/flask-sqlite/app.py b/flask-sqlite/app.py index b66f92f..aa1b3be 100644 --- a/flask-sqlite/app.py +++ b/flask-sqlite/app.py @@ -1,80 +1,150 @@ from flask import Flask, request, jsonify from models import db, Student +from werkzeug.exceptions import HTTPException +import logging +import os +from dotenv import load_dotenv app = Flask(__name__) -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///students.db' +app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URI', 'sqlite:///students.db') app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False - +app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'fallback_secret_key') db.init_app(app) -# 🔧 Validation Helper Function +# Config Logging +logging.basicConfig(level=logging.INFO) + def validate_student_data(data, partial=False): if not data: return "Missing JSON body" - if not partial: if 'name' not in data: return "Missing 'name'" if 'age' not in data: return "Missing 'age'" - if 'name' in data: if not isinstance(data['name'], str) or not data['name'].strip(): return "Invalid 'name'" - + if len(data['name'].strip()) > 100: + return "'name' too long" if 'age' in data: if not isinstance(data['age'], int): return "Invalid 'age'" - + if data['age'] < 0 or data['age'] > 150: + return "'age' out of valid range" return None # No error +# Test Route @app.route('/') def home(): - return jsonify({"message": "Flask Student API"}), 200 + return jsonify({"success": True, "message": "Flask Student API"}), 200 +# CRUD Routes +# Get all students @app.route('/students', methods=['GET']) def get_students(): + try: + page = int(request.args.get('page',1)) + per_page = int(request.args.get('per_page',10)) + except ValueError: + return jsonify({"success":False, "error":"'page' and 'per_page' must be integers"}), 400 + students = Student.query.all() - return jsonify([ - {"id": s.id, "name": s.name, "age": s.age} for s in students - ]) + +# Filtering by name partial match + name_filter = request.args.get('name') + if name_filter: + students_query = students_query.filter(Student.name.ilike(f'%{name_filter}%')) + +# Filtering by age exact match + age_filter = request.args.get('age') + if age_filter and age_filter.isdigit(): + students_query = students_query.filter(Student.age == int(age_filter)) + + pagination = students_query.paginate(page=page, per_page=per_page, error_out=False) + students = pagination.items + + return jsonify({ + "success": True, + "page": page, + "per_page": per_page, + "total": pagination.total, + "total_pages": pagination.pages, + "students":[ + {"id": s.id, "name": s.name, "age": s.age} for s in students + ]}), 200 + +# Add a new student @app.route('/students', methods=['POST']) def add_student(): - data = request.get_json() + try: + data = request.get_json(force=True) + except Exception: + return jsonify({"success": False, "error": "Invalid JSON"}), 400 error = validate_student_data(data) if error: - return jsonify({"error": error}), 400 - + return jsonify({"success": False, "error": error}), 400 student = Student(name=data['name'].strip(), age=data['age']) db.session.add(student) - db.session.commit() - return jsonify({"id": student.id}), 201 - + try: + db.session.commit() + except Exception as e: + db.session.rollback() + logging.error(f"DB Error: {e}") + return jsonify({"success": False, "error": "Database error"}), 500 + return jsonify({"success": True, "id": student.id, "name": student.name}), 201 + +# Update a student @app.route('/students/', methods=['PUT']) def update_student(id): - student = Student.query.get_or_404(id) - data = request.get_json() + student = Student.query.get(id) + if not student: + return jsonify({"success": False, "error": "Student not found"}), 404 + try: + data = request.get_json(force=True) + except Exception: + return jsonify({"success": False, "error": "Invalid JSON"}), 400 error = validate_student_data(data, partial=True) if error: - return jsonify({"error": error}), 400 - + return jsonify({"success": False, "error": error}), 400 if 'name' in data: student.name = data['name'].strip() if 'age' in data: student.age = data['age'] - - db.session.commit() - return jsonify({"message": "Updated"}), 200 - + try: + db.session.commit() + except Exception as e: + db.session.rollback() + logging.error(f"DB Error: {e}") + return jsonify({"success": False, "error": "Database error"}), 500 + return jsonify({"success": True, "message": "Updated"}), 200 + +# Delete a student @app.route('/students/', methods=['DELETE']) def delete_student(id): - student = Student.query.get_or_404(id) - db.session.delete(student) - db.session.commit() - return jsonify({"message": "Deleted"}), 200 + student = Student.query.get(id) + if not student: + return jsonify({"success": False, "error": "Student not found"}), 404 + try: + db.session.delete(student) + db.session.commit() + except Exception as e: + db.session.rollback() + logging.error(f"DB Error: {e}") + return jsonify({"success": False, "error": "Database error"}), 500 + return jsonify({"success": True, "message": "Deleted"}), 200 + +# Global Exception Handler +@app.errorhandler(Exception) +def handle_exception(e): + # Handle HTTP errors + if isinstance(e, HTTPException): + return jsonify({"success": False, "error": str(e)}), e.code + logging.error(f"Unhandled Error: {e}") + return jsonify({"success": False, "error": "Internal server error"}), 500 if __name__ == '__main__': with app.app_context(): db.create_all() - app.run(debug=True) + app.run() # Remove debug=True for production diff --git a/flask-sqlite/requirements.txt b/flask-sqlite/requirements.txt index 41fbe74..b860132 100644 --- a/flask-sqlite/requirements.txt +++ b/flask-sqlite/requirements.txt @@ -1,2 +1,3 @@ -Flask==3.0.2 -Flask_SQLAlchemy==3.1.1 +Flask +Flask_SQLAlchemy +python-dotenv \ No newline at end of file