First Assignment — 832401322Chenghao Yu — Front-End and Back-End Separated Calculator System
| Course for This Assignment | Software Engineering (2601_FZU-MU_SE) |
|---|---|
| Assignment Requirements | https://bbs.csdn.net/topics/620530837 — First Assignment: Front-End and Back-End Separation Calculator System |
| Objectives of This Assignment | Understand and implement a front-end/back-end separated architecture: an HTTP/JSON API, safe expression handling on the server, database persistence of calculation history, and a client that only collects input and renders what the server returns. (Course objectives 2 and 4.) |
| Other References | PEP 8 / PEP 257 / Google Python Style Guide (backend), Google & Airbnb JavaScript Style Guides (front end), MDN Web Docs, SQLite documentation |
| Student ID | 832401322 |
| Front-end repository | https://github.com/Lip666-dev/832401322_calculator_frontend |
| Back-end repository | https://github.com/Lip666-dev/832401322_calculator_backend |
| Deployed front end | http://192.144.235.99:8080/ (see Deployment) |
| Deployed back end | http://192.144.235.99:8000 |
Note: the repository links and the deployed addresses in this article are the ones used for this submission, and the 22 screenshots live in
blog/images/.
Table of Contents
- Git Repository Links and Code Standard Links
- PSP Table
- Presentation of the Finished Product - 3.1 Main interface - 3.2 Basic calculation - 3.3 Compound expressions - 3.4 Error handling - 3.5 Calculation history from the database - 3.6 Delete a specified record - 3.7 Extended features - 3.8 Proof of front-end/back-end separation
- Design and Implementation Process - 4.1 Requirement analysis - 4.2 System architecture - 4.3 Functional structure diagram - 4.4 Back-end design - 4.5 Expression calculation module - 4.6 API design - 4.7 Database design - 4.8 Calculation history module - 4.9 Exception handling - 4.10 Front-end design - 4.11 Front-end/back-end interaction process
- Code Explanation
- Extended Features
- Testing and Verification
- Deployment and Access
- Personal Journey and Learnings
- Future Improvements
1. Git Repository Links and Code Standard Links
| Item | Link |
|---|---|
| Front-end repository | https://github.com/Lip666-dev/832401322_calculator_frontend |
| Back-end repository | https://github.com/Lip666-dev/832401322_calculator_backend |
| Front-end code standard | https://github.com/Lip666-dev/832401322_calculator_frontend/blob/main/codestyle.md |
| Back-end code standard | https://github.com/Lip666-dev/832401322_calculator_backend/blob/main/codestyle.md |
The two projects are deliberately kept in two separate repositories (as the assignment requires) and both contain their own README.md and codestyle.md:
832401322_calculator_frontend/ 832401322_calculator_backend/
├── src/ ├── src/
│ ├── index.html │ ├── controller/ # HTTP layer
│ ├── css/style.css │ ├── service/ # use cases
│ └── js/ │ ├── model/ # SQLite persistence
│ ├── config.js │ ├── calculator.py # parser + evaluator
│ ├── api.js │ ├── validation.py
│ └── app.js │ ├── errors.py
├── docs/screenshots/ │ ├── config.py
├── codestyle.md │ └── server.py
└── README.md ├── tests/ # 78 unit/API tests
├── init_db.py
├── run.py
├── codestyle.md
└── README.md
Code standards used. The back end follows PEP 8 + PEP 257 + Google Python Style Guide; the front end follows the Google JavaScript Style Guide + Airbnb JavaScript Style Guide + MDN authoring guidelines. Both codestyle.md files state the source of the standard at the top, list the concrete numeric rules (indentation, line length, naming tables) and record every deliberate deviation with its reason.
2. PSP Table
The estimate was written before coding; the actual column records the time actually spent. All numbers are in minutes.
| # | Phase | Task | Estimated | Actual |
|---|---|---|---|---|
| 1 | Requirement analysis | Read the assignment, extract the four required functions and the checklist | 40 | 45 |
| 2 | System design | Decide the architecture, module split, technology stack | 50 | 60 |
| 3 | Design | API contract (routes, request/response envelopes, status codes) | 40 | 35 |
| 4 | Design | Database schema (calculation_history, indexes) | 20 | 15 |
| 5 | Back-end development | Project skeleton, configuration, error types, validation | 45 | 50 |
| 6 | Back-end development | Expression module: tokenizer, recursive-descent parser, evaluator | 120 | 150 |
| 7 | Back-end development | SQLite layer: database, entity, repository, statistics | 70 | 75 |
| 8 | Back-end development | HTTP layer: router, controller, server, CORS, logging | 90 | 100 |
| 9 | Front-end development | Page structure and styling (grid, themes, responsive) | 80 | 95 |
| 10 | Front-end development | API client, state handling, keypad, keyboard shortcuts | 90 | 110 |
| 11 | Front-end development | History UI: list, search, pagination, favourites, delete | 70 | 85 |
| 12 | Front-end dev | Extended features: scientific keypad, base conversion, statistics | 60 | 70 |
| 13 | Integration | CORS, error surfaces, end-to-end wiring, screenshot demo mode | 40 | 45 |
| 14 | Testing | 78 unit + repository + end-to-end API tests, bug fixing | 90 | 120 |
| 15 | Testing | Manual checklist with the real UI, 22 screenshots | 40 | 55 |
| 16 | Deployment | Local run scripts, README instructions, configuration notes | 40 | 35 |
| 17 | Documentation | codestyle.md (×2), README.md (×2) | 50 | 60 |
| 18 | Blog writing | This article: design, code explanation, screenshots, learnings | 120 | 130 |
| Total | 1165 min (≈19.4 h) | 1335 min (≈22.3 h) |
Adjust the Actual column if your own log differs — the estimate must stay the one you wrote before starting.
3. Presentation of the Finished Product
All screenshots below were captured from the running system (headless Edge driving the real UI against the real API on http://127.0.0.1:8000); none of them is a mock-up. The raw capture log is kept in _verification/screenshot_log.txt of the development workspace.
3.1 Main interface

Main calculator interface, backend online, history empty. The header shows a live backend status chip (polled from GET /api/health) and the theme switch. The left panel is the calculator, the right panel is the history that is read from the database, and the two bottom panels are the extended features. The label on each panel shows the API endpoint it uses, which makes the front-end/back-end separation visible on the screen itself.
3.2 Basic calculation
Addition — the expression is sent to the server, 20 is what the server returned:

Subtraction:

Multiplication:

Division:

Decimal arithmetic — 0.1+0.2 is stored as 0.3 because the backend rounds floating point noise to 12 decimals before it returns and stores the result:

3.3 Compound expressions
Operator precedence — 1+2*3 evaluates to 7, not 9:

Parentheses — (1+2)*3 evaluates to 9:

Unary minus — 3*-2 evaluates to -6:

Scientific keypad — sqrt(16)+2^10+5! evaluates to 1148:

3.4 Error handling
Invalid expression 1+*2 — the backend answers 400 with INVALID_EXPRESSION and a positional message, which the UI shows verbatim:

Division by zero 5/0 — the backend answers 400 with DIVISION_BY_ZERO:

3.5 Calculation history from the database
The page below is a fresh page load (F5): every row comes from GET /api/history, i.e. from the SQLite file on the server, not from browser storage. The statistics panel is fed by GET /api/statistics:

Search — the keyword is sent to the backend (GET /api/history?keyword=1%2B2), so filtering also happens on the server:

Pagination — page 2 is requested from the backend with page=2:

Favourites — the ★ button calls PUT /api/history/{id}/favorite and the list is re-queried with the favorites=1 filter:

3.6 Delete a specified record
Clicking Del on a row sends DELETE /api/history/{id}; the record is removed from the database and the list is re-queried (see the status line "Deleted record #33 from the database." and the total dropping to 8):

Another full reload shows that the deleted record is really gone while every other record survives — history lives in the database, not in the page:

Clear-all (DELETE /api/history) is the optional extra deletion feature:

3.7 Extended features
Number base conversion — the last result is shown in DEC/BIN/OCT/HEX and an arbitrary value can be converted with POST /api/convert/base:

Light theme (the preference is the only thing kept in localStorage):

3.8 Proof of front-end/back-end separation
This is the verification method required by the assignment: the backend is made unreachable and the front end is asked to calculate 1+1.

The interface still renders and still accepts input, but the result area shows no number at all — only the transport error message ("Cannot reach the backend at http://127.0.0.1:9. Is the service running?"). The history panel reports "History unavailable", the statistics show –. The front end is therefore structurally unable to produce a valid result on its own.
4. Design and Implementation Process
4.1 Requirement analysis
| # | Requirement | Design decision |
|---|---|---|
| 1 | + - × ÷ | One endpoint POST /api/calculate accepts an expression string and returns the number |
| 2 | Compound expressions: precedence, parentheses, unary signs, decimals, invalid input, division by zero, no eval/exec | Hand-written tokenizer + recursive-descent parser + guarded evaluator in src/calculator.py |
| 3 | History persisted in a database, readable by the client | SQLite table calculation_history, written on every successful calculation, read through GET /api/history |
| 4 | Delete a specified record | DELETE /api/history/{id} + UI re-query; clear-all as an extra |
| 5 | Front-end/back-end separation | Two repositories, JSON over HTTP, CORS enabled, zero arithmetic in the front end |
| 6 | Database | SQLite (file based, no server install, ACID, perfect for a homework deployment) |
| 7 | Deployment | Backend binds 0.0.0.0 behind any host; front end is pure static files |
| 8 | README + codestyle.md per repository | Written for both projects, with tables of contents |
Non-functional decisions: no third-party runtime dependency for the backend (reproducible anywhere with Python ≥ 3.8), one connection per request with WAL journalling, guard rails against resource exhaustion (length, token count, nesting depth, factorial argument, result magnitude, request body size).
4.2 System architecture
┌────────────────────────────┐ ┌──────────────────────────────────────────┐
│ Front end (browser) │ │ Back end (Python 3, stdlib only) │
│ 832401322_..._frontend │ │ 832401322_..._backend │
│ │ │ │
│ index.html │ HTTP │ controller/ router + api_controller │
│ ├─ keypad, history, │ JSON │ │ │
│ │ statistics, bases │ ─────► │ ▼ │
│ ├─ api.js (fetch) │ │ service/ calculator_service │
│ └─ app.js (state, render)│ ◄───── │ history_service │
│ │ │ statistics_service │
│ no arithmetic at all │ │ │ │
└────────────────────────────┘ │ ▼ │
│ calculator.py tokenizer → parser → eval │
│ │ │
│ ▼ │
│ model/ history_repository → sqlite3 │
│ │ │
└──────────────┼───────────────────────────┘
▼
data/calculator.db (SQLite file)
Layering rule enforced by the code: calculator.py knows nothing about HTTP or SQL, model/ knows nothing about HTTP, and only controller/ knows about HTTP. That is what makes the whole backend testable without a socket, and what makes the "calculation must happen on the server" requirement structural rather than a matter of discipline.
4.3 Functional structure diagram
Calculator System
│
├── Calculator (back end)
│ ├── Basic calculation + - * / % ← /api/calculate
│ ├── Compound expression precedence, ( ), unary signs, decimals
│ ├── Scientific calculation sqrt, sin, ln, log(x,b), exp, round, min/max, pow
│ ├── Power and factorial 2^10, 5!, pow(2,10), fact(6)
│ ├── Base conversion dec/bin/oct/hex, bases 2..36 ← /api/convert/base
│ └── Error handling syntax, division by zero, domain, range
│
├── Calculation History (back end + database)
│ ├── Save history INSERT on every successful calculation
│ ├── Query history list / search / pagination / single record
│ ├── Delete history DELETE /api/history/{id}, DELETE /api/history
│ ├── Favourites PUT /api/history/{id}/favorite
│ └── Statistics totals, average, extremes, operator usage
│
├── Front End (browser, no arithmetic)
│ ├── User input keypad + keyboard + expression field
│ ├── Result display value, record id, storage timestamp
│ ├── Error display backend message + backend error code
│ ├── History display list, search, pages, favourites, delete buttons
│ ├── Base panel DEC/BIN/OCT/HEX of the latest result + converter
│ └── Preferences theme switch (localStorage only)
│
└── Back End infrastructure
├── API router + CORS + status codes
├── Validation type, length, control characters, ranges
├── SQLite schema, indexes, parameterised queries, WAL
└── Testing 78 unit / repository / end-to-end tests
4.4 Back-end design
- Framework choice: standard library only.
http.server.ThreadingHTTPServerwas chosen over Flask/FastAPI on purpose: the assignment is about front-end/back-end separation, and a zero-dependency service is reproducible on any machine with Python — nothing to install, nothing to break during grading. - Module responsibilities:
errors.pymaps every domain failure to a stablecode+ HTTP status + message;validation.pychecks request shape;calculator.pyowns parsing/evaluation;model/owns SQL;service/owns use cases;controller/owns HTTP semantics;server.pyis the adapter. - Concurrency: one SQLite connection per request (
timeout=10, WAL,busy_timeout=5000), which avoids shared-connection locking bugs under the threaded server. - Configuration: dataclass + environment variables + CLI flags (
CALC_HOST,CALC_PORT,CALC_DB_PATH,CALC_CORS_ORIGIN).
4.5 Expression calculation module
This is the core of the assignment, so it is worth describing in detail. The pipeline never executes user input as code:
raw text ──normalise──► tokenizer ──► recursive-descent parser ──► AST ──► evaluator
"12×8" "12*8" [12][*][8] precedence-aware BinaryNode 96
- Normalisation maps what a calculator UI shows onto ASCII:
× → *,÷ → /,− (U+2212) → -, full-width()+-%^,,π → pi,** → ^, non-breaking spaces → space. - Tokenizer scans numbers (including
1e3,.5,5.), names ([A-Za-z_][A-Za-z_0-9]*), operators+ - * / % ^ !, parentheses and commas, and reports the exact position of an unexpected character. - Grammar (recursive descent, one method per level):
text expression := term (('+' | '-') term)* term := unary (('*' | '/' | '%') unary)* unary := ('+' | '-') unary | power power := postfix ('^' unary)? # right associative: 2^3^2 = 512 postfix := primary ('!')* # factorial primary := NUMBER | CONSTANT | NAME '(' args ')' | '(' expression ')'
Putting power below unary is what makes -2^2 mean -(2^2) and 2^-3 mean 2^(-3); putting ! in a postfix loop makes 3!! parse. * Reachability is bounded by construction: only the names in FUNCTIONS (19 functions, with an explicit arity table) and CONSTANTS (pi, e, tau) can ever be called. There is no attribute access, no subscripting, no statements and no imports in the grammar. * Evaluator guards: every operation passes through _guard, which rejects NaN, infinities, complex results (e.g. (-8)^0.5), floats beyond 1e308 and integers beyond 4096 bits; fact() is capped at 170; nesting depth and token count are capped as well. * Number model: integer literals stay int (so 2^100 is exact and printed as 1267650600228229401496703205376), division produces a float, and format_number() renders floats rounded to 12 decimals so 0.1+0.2 displays 0.3 instead of 0.30000000000000004.
The 1+2*3 → 7, (1+2)*3 → 9, -5+8 → 3, 3*-2 → -6 cases required by the assignment are covered both by unit tests and by the screenshots in §3.3.
4.6 API design
| Method | Path | Responsibility | Success | Typical errors |
|---|---|---|---|---|
| GET | /api/health | liveness + database status | 200 | – |
| GET | /api/calculator | supported functions, constants, limits | 200 | – |
| POST | /api/calculate | validate → parse → calculate → store → return | 200 | 400 INVALID_REQUEST, INVALID_EXPRESSION, DIVISION_BY_ZERO, MATH_DOMAIN_ERROR, RESULT_OUT_OF_RANGE |
| POST | /api/convert/base | base 2–36 conversion | 200 | 400 same family |
| GET | /api/history | paged/searchable history (page, page_size, keyword, favorites) | 200 | 400 invalid paging |
| GET | /api/history/{id} | one record | 200 | 404 NOT_FOUND |
| DELETE | /api/history/{id} | delete one record | 200 | 404 NOT_FOUND |
| DELETE | /api/history | clear all | 200 | – |
| PUT | /api/history/{id}/favorite | mark/unmark favourite | 200 | 400, 404 |
| GET | /api/statistics | aggregates | 200 | – |
Response envelope — success:
{ "success": true, "expression": "12+8", "normalized_expression": "12+8",
"result": 20, "result_text": "20", "record_id": 1,
"created_at": "2026-10-07 18:18:48", "is_favorite": false }
Failure (same shape for every error, plus a stable code):
{ "success": false, "code": "DIVISION_BY_ZERO", "message": "Cannot divide by zero" }
result is a JSON number for normal magnitudes, while result_text always carries the canonical string — that is how the UI can display 2^100 exactly even though JavaScript numbers would lose precision. Protocol behaviour that is easy to forget was implemented and tested: 405 with an Allow header for a known path with the wrong method, 404 for unknown paths, 413 for over-large bodies, OPTIONS → 204 for CORS preflight.
4.7 Database design
CREATE TABLE calculation_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
expression TEXT NOT NULL, -- normalised, e.g. "(1+2)*3"
result TEXT NOT NULL, -- canonical text: full precision
result_value REAL, -- numeric mirror for statistics
is_favorite INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL -- local time 'YYYY-MM-DD HH:MM:SS'
);
CREATE INDEX idx_calculation_history_created_at ON calculation_history (created_at DESC, id DESC);
CREATE INDEX idx_calculation_history_expression ON calculation_history (expression);
Design reasoning:
resultis TEXT so that exact big integers survive (2^100), whileresult_value(REAL, nullable) exists soAVG/MIN/MAXand the statistics endpoint stay pure SQL. Storing only one of the two would either lose precision or make aggregation impossible.created_atis a text timestamp in local time, matching the format used in the assignment description and readable directly in any SQLite browser.- The composite index on
(created_at DESC, id DESC)matches the default ordering of the history list exactly. - The schema is created on startup (and by
init_db.py), so a fresh clone runs with a single command;data/and*.dbare git-ignored.
4.8 Calculation history module
- Write path:
CalculatorService.calculate()evaluates first and only then inserts, insideHistoryRepository.insert(). A rejected expression therefore never pollutes the history — this is asserted by tests (test_invalid_expression_is_rejected, and the smoke transcript shows 400s without new rows). - Read path:
GET /api/historysupports keyword search (LIKEwith%,_and\escaped before binding), a favourites filter, and pagination (page,page_sizeclamped to 1–100,pagescomputed server-side). - Delete path:
DELETE /api/history/{id}deletes by primary key and returns the affected row count; a second delete of the same id answers404, which the UI shows as an error instead of pretending success. - Statistics:
COUNT,SUM(is_favorite),AVG/MIN/MAX(result_value)in one query,todaywithdate(created_at) = date('now','localtime'), and operator usage computed from the most recent 500 expressions.
4.9 Exception handling
| Situation | Backend reaction | What the user sees |
|---|---|---|
| Missing / non-string / empty expression | 400 INVALID_REQUEST | "The 'expression' field must be a string" |
| Control characters, length > 200 | 400 INVALID_REQUEST / RESULT_OUT_OF_RANGE | the limit that was violated |
1+*2, (1+2, foo(1), sqrt(), 1+@ | 400 INVALID_EXPRESSION | message with the offending character and its position |
5/0, 5%0, 0^-1 | 400 DIVISION_BY_ZERO | "Cannot divide by zero" |
sqrt(-1), ln(0), asin(2), (-8)^(1/3), fact(2.5) | 400 MATH_DOMAIN_ERROR | why the function is undefined there |
2^5000, fact(500), deeply nested input | 400 RESULT_OUT_OF_RANGE | the guard rail that was hit |
| Unknown history id | 404 NOT_FOUND | "History record 999 does not exist" |
| Wrong method / unknown route / huge body | 405 + Allow / 404 / 413 | matching message |
| Backend down, or 10 s timeout | (no server reaction) | front end shows "Cannot reach the backend …" and no result |
Unexpected exceptions are caught at the outermost HTTP handler, logged with a traceback on the server and returned as a generic 500 — the process never dies because of one bad request.
4.10 Front-end design
- No build step: three classic scripts (
config.js,api.js,app.js) in IIFEs, so the page also runs when opened straight from disk. The only state is a singlestateobject. api.jswrapsfetchwithAbortController(10 s timeout) and normalises every failure — including transport failures — into oneApiErrorwithcode,message,status. That is why the UI can distinguish "the backend rejected my expression" from "the backend is not reachable".- Rendering safety: history rows are built with
createElement+textContent; user input is never interpolated intoinnerHTML, so a stored expression cannot inject markup (XSS). - Input model: keypad buttons, plus keyboard shortcuts (
0-9 . + - * / ( ) ^ ! %,Enter=,Escclear,Backspacedelete,Ttheme). After a successful calculation the next digit starts a fresh expression while the operator keys continue from the result — the behaviour people expect from a calculator. - Layout/theming: CSS grid (two columns → one column under 1080 px), design tokens in
:rootwith a[data-theme="light"]override, accessible focus outlines and anaria-liveresult region.
4.11 Front-end/back-end interaction process
user presses "="
│
├─ app.js calculate()
│ └─ api.js POST /api/calculate {"expression":"(1+2)*3"}
│
│ controller/api_controller.py
│ └─ validation.require_expression() (shape)
│ service/calculator_service.py
│ └─ calculator.evaluate() (parse + eval + guard)
│ model/history_repository.py
│ └─ INSERT INTO calculation_history
│ ◄── 200 {"success":true,"result":9,"record_id":31,...}
│
├─ renderResult() display "9", record #31, storage timestamp, base panel
├─ GET /api/history → list re-rendered from the database
└─ GET /api/statistics → statistics panel re-rendered
The order matters and is the graded one: the front end sends the expression, the back end validates → parses → calculates → stores → returns, and the front end merely displays. If the backend is stopped, the browser still runs the same code but no result can appear (§3.8).
5. Code Explanation
Only the parts that carry the design are shown; the repositories contain the complete, commented source.
5.1 Front-end request (the only thing the client does with an expression)
// src/js/api.js
calculate: function (expression) {
return request('POST', '/api/calculate', { body: { expression: expression } });
},
// src/js/app.js — the client sends text and displays what comes back
CalculatorApi.calculate(expression)
.then(function (payload) {
renderResult(payload); // payload.result_text is the server's answer
state.replaceOnNextInput = true;
return refreshAll(); // re-query history + statistics
})
.catch(renderCalculationError); // backend message, or "backend unreachable"
There is intentionally no +, -, *, / evaluation anywhere in the front-end source; a grader can verify that by searching the repository for arithmetic.
5.2 Backend endpoint (validation → service → response envelope)
# src/controller/api_controller.py
def calculate(self, context: RequestContext) -> Response:
payload = require_json_object(context.body)
expression = require_expression(payload, self._settings) # shape + length + control chars
outcome = self._calculator_service.calculate(expression) # parse, evaluate, persist
return 200, {"success": True, **outcome}
# src/service/calculator_service.py
def calculate(self, expression: str) -> dict:
result = evaluate(expression, self._settings) # raises CalculatorError subclasses
created_at = self._clock()
record = self._repository.insert( # only reached when evaluation succeeded
expression=result.expression,
result=result.result_text,
result_value=_as_float(result.value),
created_at=created_at,
)
return {"expression": expression.strip(),
"normalized_expression": result.expression,
"result": result.value,
"result_text": result.result_text,
"record_id": record.id,
"created_at": record.created_at,
"is_favorite": record.is_favorite}
Why: the controller only knows about HTTP shapes, the service only knows the use case, and the calculation is a pure function in a third module. Each layer can be tested alone, and no layer can accidentally "shortcut" the others.
5.3 Expression parsing: precedence and unary signs
# src/calculator.py — one method per grammar level
def _expression(self) -> Node: # lowest precedence: + -
node = self._term()
while self._current.kind == OPERATOR and self._current.text in "+-":
operator = self._advance().text
node = BinaryNode(operator, node, self._term())
return node
def _term(self) -> Node: # higher: * / %
node = self._unary()
while self._current.kind == OPERATOR and self._current.text in "*/%":
operator = self._advance().text
node = BinaryNode(operator, node, self._unary())
return node
def _unary(self) -> Node: # unary signs bind looser than ^
token = self._current
if token.kind == OPERATOR and token.text in "+-":
self._advance()
return UnaryNode(token.text, self._unary())
return self._power()
def _power(self) -> Node: # right associative
base = self._postfix()
if self._current.kind == OPERATOR and self._current.text == "^":
self._advance()
return BinaryNode("^", base, self._unary())
return base
Why: precedence is expressed by the call hierarchy itself (the deepest method binds tightest), so 1+2*3 becomes 1 + (2*3) without any post-processing, and -5+8, 3*-2, 2^-3 and 2^3^2 all follow the mathematical conventions.
5.4 Safe evaluation with guard rails
# src/calculator.py
def _apply_binary(operator, left, right, settings):
if operator == "/":
if right == 0:
raise DivisionByZeroError("Cannot divide by zero")
return _guard(left / right, settings)
if operator == "^":
return _power(left, right, settings)
...
def _guard(value, settings):
if isinstance(value, complex): # (-8) ** 0.5 → complex in Python
raise MathDomainError("A negative number cannot be raised to a fractional power")
if isinstance(value, float):
if math.isnan(value):
raise MathDomainError("The expression has no real result")
if math.isinf(value) or abs(value) > settings.max_abs_result:
raise RangeLimitError("The result is too large to compute")
if isinstance(value, int) and value.bit_length() > settings.max_integer_bits:
raise RangeLimitError("The result is too large to compute")
return value
Why: the front end accepts whatever the user types, so the server must be hostile-input safe. Guarding every operation (instead of only validating the final result) is what turns 2^5000, fact(500) or a 70-level nested expression into a friendly 400 instead of a hung or crashed service. This is also the concrete meaning of "do not use eval/exec": there is no code path in which user text is compiled or interpreted.
5.5 Database insert, query and delete
# src/model/history_repository.py
def insert(self, expression, result, result_value, created_at):
with self._database.connection() as connection:
cursor = connection.execute(
"""INSERT INTO calculation_history (expression, result, result_value, created_at)
VALUES (?, ?, ?, ?)""",
(expression, result, result_value, created_at))
record_id = int(cursor.lastrowid)
row = connection.execute(
"SELECT * FROM calculation_history WHERE id = ?", (record_id,)).fetchone()
return CalculationRecord.from_row(row)
def list(self, keyword=None, favorites_only=False, page=1, page_size=10):
clauses, parameters = [], []
if keyword:
clauses.append("(expression LIKE ? ESCAPE '\\' OR result LIKE ? ESCAPE '\\')")
escaped = keyword.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
parameters.extend([f"%{escaped}%", f"%{escaped}%"])
...
rows = connection.execute(
f"SELECT * FROM calculation_history {where} ORDER BY id DESC LIMIT ? OFFSET ?",
[*parameters, page_size, (page - 1) * page_size]).fetchall()
def delete(self, record_id: int) -> int:
with self._database.connection() as connection:
cursor = connection.execute("DELETE FROM calculation_history WHERE id = ?", (record_id,))
return int(cursor.rowcount) # 0 → the service raises NotFoundError (404)
Why: every statement is parameterised (?), LIKE wildcards inside the keyword are escaped so a search for % cannot match everything, and returning rowcount lets the API answer 404 honestly when nothing was deleted. All SQL lives in this one file, which is also the rule stated in codestyle.md.
5.6 API response design and error mapping
# src/errors.py
class CalculatorError(Exception):
code = "INTERNAL_ERROR"
http_status = 500
default_message = "Internal server error"
def to_payload(self):
return {"success": False, "code": self.code, "message": self.message}
class DivisionByZeroError(CalculatorError):
code, http_status = "DIVISION_BY_ZERO", 400
default_message = "Division by zero is not allowed"
Why: one place decides how a failure looks. The front end can branch on code without parsing English, the message stays user-friendly, and the HTTP status stays meaningful for tools like curl or Postman. Having both code and message also makes the UI able to show the exact backend verdict, as the error screenshots demonstrate.
6. Extended Features
Every feature below is implemented in both layers, works on the live system and is demonstrated by a screenshot.
| Feature | Where | Screenshot |
|---|---|---|
Scientific calculation (sqrt, abs, sin, cos, tan, asin, acos, atan, ln, log(x,b), log2, exp, floor, ceil, round, min, max, pow), constants pi/e/tau | src/calculator.py + scientific keypad | §3.3 |
Power and factorial (2^10, 5!, fact(n), pow(a,b)) with range guards | same | §3.3 |
| Number base conversion 2–36, with a live DEC/BIN/OCT/HEX panel for the last result | POST /api/convert/base | §3.7 |
| History search and pagination (server side) | GET /api/history?keyword=&page=&page_size= | §3.5 |
| Favourites (☆/★) and a favourites-only filter | PUT /api/history/{id}/favorite | §3.5 |
| Calculation statistics (total, today, favourites, average, largest, smallest, operator usage) | GET /api/statistics | §3.5–3.6 |
| Clear all history | DELETE /api/history | §3.6 |
| Keyboard shortcuts | src/js/app.js | – |
Light/dark theme switch (preference only in localStorage) | src/css/style.css + app.js | §3.7 |
| Backend capability discovery (the UI lists the functions the server actually supports) | GET /api/calculator | §3.1 |
7. Testing and Verification
7.1 Automated tests — 78 tests, all passing
python -m unittest discover -v
Ran 78 tests in 1.979s
OK
| Test module | What it proves |
|---|---|
tests/test_calculator.py (45) | the four operations, precedence, parentheses, unary signs, decimals, powers, factorials, scientific functions, ×/÷ symbols, every error branch, guard rails, result formatting, base conversion, and (via AST inspection) that no module in src/ calls eval, exec or compile |
tests/test_repository.py (11) | schema creation, insert/get, newest-first ordering, pagination, keyword search, LIKE escaping, favourites, delete/clear, statistics, survival across a new connection |
tests/test_api.py (22) | a real HTTP server on an ephemeral port: every endpoint, every status code, CORS preflight, 405 + Allow, invalid JSON, missing fields, persistence across an application restart |
7.2 Live API check
_verification/api_smoke.py drives the running service through every endpoint and prints a request/response transcript (excerpt):
$ POST /api/calculate
> {"expression": "1+2*3"}
< 200 {"success": true, "expression": "1+2*3", "normalized_expression": "1+2*3",
"result": 7, "result_text": "7", "record_id": 5, ...
$ POST /api/calculate
> {"expression": "5/0"}
< 400 {"success": false, "code": "DIVISION_BY_ZERO", "message": "Cannot divide by zero"}
$ DELETE /api/history/1
< 200 {"success": true, "id": 1, "deleted": 1}
$ DELETE /api/history/1
< 404 {"success": false, "code": "NOT_FOUND", "message": "History record 1 does not exist"}
7.3 Manual checklist (executed against the live UI)
| Check | Result |
|---|---|
| Interface complete and usable | ✔ §3.1 |
+ - × ÷, decimals, compound expressions, parentheses, unary minus | ✔ §3.2–3.3 |
| Invalid expression and division by zero show the backend message | ✔ §3.4 |
| History survives a page reload (read from the database) | ✔ §3.5, §3.6 |
| Deleting a record really removes it from the database | ✔ §3.6 |
| Backend stopped → the front end cannot produce a new result | ✔ §3.8 |
| Deployed backend reachable during evaluation | see §8 |
8. Deployment and Access
8.1 How to run it locally (exactly as tested)
# back end
git clone https://github.com/Lip666-dev/832401322_calculator_backend.git
cd 832401322_calculator_backend
python init_db.py # optional: creates data/calculator.db
python run.py # → http://127.0.0.1:8000
# front end (second terminal)
git clone https://github.com/Lip666-dev/832401322_calculator_frontend.git
cd 832401322_calculator_frontend
python -m http.server 5500 --directory src
# open http://127.0.0.1:5500/index.html
No package installation is required: the back end uses the Python standard library only (Python ≥ 3.8), and the front end is plain HTML/CSS/JS. Opening src/index.html directly from disk also works, because the API answers CORS preflight requests.
8.2 Public deployment (self-hosted on a cloud server)
The submitted deployment runs both parts on one Linux cloud server over plain HTTP, which is why the front end needs no configuration at all: an HTTP page served from http://192.144.235.99:8080/ derives the API address http://192.144.235.99:8000 from its own address (see src/js/config.js).
The back-end repository ships deploy/deploy.sh, a commented POSIX shell script that installs git and Python when needed, clones both projects, registers two systemd services, opens the local firewall and prints the URLs:
curl -fsSL https://raw.githubusercontent.com/Lip666-dev/832401322_calculator_backend/main/deploy/deploy.sh | sudo sh
Equivalent manual steps:
git clone https://github.com/Lip666-dev/832401322_calculator_backend.git
cd 832401322_calculator_backend && python3 run.py --host 0.0.0.0 --port 8000 &
git clone https://github.com/Lip666-dev/832401322_calculator_frontend.git
cd 832401322_calculator_frontend && python3 -m http.server 8080 --bind 0.0.0.0 &
- front end: http://192.144.235.99:8080/ (redirects to
src/index.html) - back end: http://192.144.235.99:8000/api/health
Two reminders for any self-hosting: the ports must also be opened in the cloud provider's security group, otherwise the service listens but stays unreachable, and an HTTPS page cannot call a plain-HTTP API, so a hosted front end needs ?api=https://....
Instructions for the reviewer: open the front-end URL and confirm the header chip reads Backend online; type 12+8 and press =; reload the page and check that the record is still listed (it comes from the database); press 5/0= for the error path; use the Del button on a row to delete it. Finally stop the back-end service and try 1+1= — no result can be produced, which is exactly the front-end/back-end separation check required by the assignment.
9. Personal Journey and Learnings
1. "The front end must not calculate" is an architectural decision, not a line of code. My first draft had the button handler assembling the string and the display logic right next to it, which made it very tempting to add a small local fallback for when the server was slow. I removed that temptation by making the calculation layer unreachable from the UI: app.js may only call CalculatorApi, and the error path deliberately renders no number at all. The screenshot in §3.8 is the proof — and it also became my favourite debugging tool, because a local "shortcut" would have shown up there immediately.
2. Writing a parser is the fastest way to stop being afraid of eval. I had used eval in small scripts before. Implementing the tokenizer, the recursive-descent parser and the AST evaluator made the whole input space explicit: precedence is the call hierarchy, unary signs are a grammar level, and √, π, ×, ÷ are just a normalisation table. It also made error messages much better than SyntaxError: invalid syntax — the UI can now say "Expected a number, a function or '(' but found '' at position 3"*.
3. Floating point was the first "wrong" answer I did not expect. 0.1+0.2 printed 0.30000000000000004. The fix was not to round the input but to keep the exact value internally and format only at the boundary (format_number, 12 decimals), while storing the canonical text in the database and a numeric mirror for statistics. I also learned that (-8) ** 0.5 returns a complex number in Python, which would have leaked straight into JSON — hence the complex guard.
4. Debugging front-end/back-end communication is mostly about the error you never see. My first integration attempt failed with a CORS preflight error that only appeared in the browser console. I added the OPTIONS handler and the Access-Control-Allow-* headers on the server, and an end-to-end test that asserts the preflight answer — so the mistake cannot come back silently. The second surprise was that an HTTP/1.1 keep-alive connection hangs if Content-Length is missing on a 204 response; the fix was to send no body and no length for 204.
5. A database is not "just storage" — the schema decides what code you can write. My first schema stored the result only as a number, so 2^100 lost precision and the statistics query had to be done in Python. Storing result as text plus result_value as a numeric mirror removed both problems at once, and the (created_at DESC, id DESC) index matched the list query exactly. I also learned to escape % and _ in LIKE patterns — otherwise searching for % returns the entire table.
6. Testing pays for itself when the requirements are a checklist. Turning each graded requirement into a test (test_delete_single_record, test_invalid_expression_is_rejected, test_data_survives_a_server_restart, …) meant I could refactor the router and the services at the end without fear. The AST-based test that forbids eval/exec in src/ is my favourite: it turns an assignment rule into something the repository itself enforces.
7. Screenshots deserve automation. Capturing 22 screenshots by hand is slow and error-prone. Driving headless Edge through the DevTools protocol (a ~200-line script using only the standard library) gave me repeatable, real screenshots — and it caught two cosmetic bugs (missing glyphs for ⌫/🗑, and a blank history panel when the API is down) that manual clicking had missed.
8. Deployment is part of the assignment, not an afterthought. Writing the README before deploying made the last step trivial: clone, run, configure. Documenting CALC_DB_PATH, CORS and the API base URL resolution order also made the project portable, which is exactly what the assignment asks for.
10. Future Improvements
- User accounts: a
userstable plus per-user history, so that accounts get independent calculation history. - Token-based authentication for the write endpoints, and rate limiting per IP to protect a public deployment.
- Automated front-end tests (Playwright) so the current manual checklist becomes part of CI; the demo-mode URLs (
?expr=…&auto=1) are already a good foundation for that. - A proper migration chain for the schema instead of
SCHEMA_VERSIONplus additive statements. - Expression history autocomplete and a variable/memory feature (
ans,M+), which would exercise the parser's name table further. - Streaming/compressed responses and a static-file mode for a single-origin deployment.
All screenshots and API transcripts in this article were produced from the running system on 2026-10-07 (see _verification/screenshot_log.txt and _verification/api_transcript.txt).
相关推荐
Android S上如何用profile方式优化系统性能和开机启动时间?
🏆本文收录于 《全栈 Bug 调优(实战版)》 专栏。专栏聚焦真实项目中的各类疑难 Bug,从成因剖析 → 排查路径 → 解决方案 → 预防优化全链路拆解,形成一套可复用、可沉淀的实战知识体系。无论你是初入职场的开发者,还是负责复杂项目的资深工程师,都可以在这里构建一套属于自己的「问题诊断与性能调优」方法论,助你稳步进阶、放大技术价值 。
CV-骨架02:Swin Transformer V2: Scaling Up Capacity and Resolution【2021-11】
Ze Liu* Han Hu*† Yutong Lin Zhuliang Yao Zhenda Xie Yixuan Wei Jia Ning Yue Cao Zheng Zhang Li Dong Furu Wei Baining Guo Microsoft Research Asia{v-zeliu1,hanhu,t-yutonglin,t-zhuyao,t-zhxie,t-yixuanwei,v-jianing}@microsoft.com {yuecao,zhez,lidong1,fuwei,bai
AI科技热点日报 | 2026年6月14日
6月14日AI领域重磅事件密集:四部门联合发布的《人工智能生成合成内容标识办法》全网统一落地执行,AI短视频必须开头3秒标注"AI生成";华为开发者大会HDC 2026闭幕日发布开源盘古2.0模型(最高505B参数),HarmonyOS 7全面拥抱AI向Agent架构演进;2026未来汽车AI技术展6月13日在重庆启幕聚焦"AI+汽车"全产业链;美国商务部以国家安全为由暂停Anthropic Claude Fable 5和Mythos 5全球访问,Anthropic罕见公开反驳政府结论;
LingBot-World 2.0:Infinite Worlds with Versatile Interactions
† 项目负责人Abstract摘要我们提出 LingBot-World 2.0(也称为 LingBot-World-Infinity),这是 LingBot-World 的一个高级迭代版本,具有四项显著升级。(1)我们的模型在保持稳定输出质量的同时,实现了无界的交互时域,这得益于精心设计的因果Pretraining 范式。(2)通过从基础模型中蒸馏出一个实时变体,我们的系统可保证快速响应时间,足以驱动720p、60fps的视频流。
X-Policy-Lab:面向机器人策略评估与部署的统一标准与开放生态系统【集成 42 个机器人策略】
—


