Skip to content

Commit a93d88d

Browse files
committed
solution for homework
bundan sonrasi fazla olucak
1 parent caeac4d commit a93d88d

11 files changed

Lines changed: 132 additions & 37 deletions

File tree

2.31 KB
Binary file not shown.
5.49 KB
Binary file not shown.
151 Bytes
Binary file not shown.
284 Bytes
Binary file not shown.

‎backend/app.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
class IndexRequest(BaseModel):
1818
origin: str
1919
k: int
20+
max_urls_to_visit: int | None = None
2021
rate_limit_per_sec: float | None = None
2122

2223

@@ -48,6 +49,7 @@ class JobDetailResponse(BaseModel):
4849
id: str
4950
origin_url: str
5051
max_depth: int
52+
max_urls_to_visit: int | None = None
5153
created_at: str
5254
updated_at: str
5355
status: str
@@ -87,12 +89,15 @@ class MetricsResponse(BaseModel):
8789
async def index(request: IndexRequest) -> IndexResponse:
8890
if request.k < 0:
8991
raise HTTPException(status_code=400, detail="k must be non-negative")
92+
if request.max_urls_to_visit is not None and request.max_urls_to_visit <= 0:
93+
raise HTTPException(status_code=400, detail="max_urls_to_visit must be positive")
9094

9195
job_id = str(uuid.uuid4())
9296
job = CrawlJob(
9397
id=job_id,
9498
origin_url=request.origin,
9599
max_depth=request.k,
100+
max_urls_to_visit=request.max_urls_to_visit,
96101
created_at=datetime.utcnow(),
97102
status=JobStatus.PENDING,
98103
)
@@ -140,6 +145,7 @@ def _serialize_job(job_id: str) -> JobDetailResponse:
140145
id=job.id,
141146
origin_url=job.origin_url,
142147
max_depth=job.max_depth,
148+
max_urls_to_visit=job.max_urls_to_visit,
143149
created_at=job.created_at.isoformat(),
144150
updated_at=job.updated_at.isoformat(),
145151
status=job.status.value,

‎backend/crawler.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ class CrawlContext:
8282
state_dirty: bool = False
8383
checkpoint_counter: int = 0
8484
checkpoint_ts: float = 0.0
85+
max_reached_notified: bool = False
8586

8687

8788
class CrawlerService:
@@ -302,6 +303,15 @@ async def _worker(self, ctx: CrawlContext) -> None:
302303

303304
async def _try_mark_visited(self, ctx: CrawlContext, url: str, depth: int) -> bool:
304305
async with self._lock:
306+
if ctx.job.max_urls_to_visit is not None and len(ctx.visited) >= ctx.job.max_urls_to_visit:
307+
if not ctx.max_reached_notified:
308+
self._append_event(
309+
ctx.job.id,
310+
"info",
311+
f"Max URLs reached ({ctx.job.max_urls_to_visit}); skipping remaining queue entries",
312+
)
313+
ctx.max_reached_notified = True
314+
return False
305315
if url in ctx.visited:
306316
self._append_event(ctx.job.id, "debug", "Skipped already-visited URL", url=url, depth=depth)
307317
return False
@@ -337,6 +347,8 @@ async def _enqueue(self, ctx: CrawlContext, url: str, depth: int, origin_url: st
337347
return False
338348
if await self._already_visited(ctx, url):
339349
return False
350+
if not await self._can_add_more_urls(ctx):
351+
return False
340352
while True:
341353
if ctx.stop_requested or ctx.job.status != JobStatus.RUNNING:
342354
return False
@@ -359,6 +371,12 @@ async def _enqueue(self, ctx: CrawlContext, url: str, depth: int, origin_url: st
359371
self._update_global_stats()
360372
await asyncio.sleep(0)
361373

374+
async def _can_add_more_urls(self, ctx: CrawlContext) -> bool:
375+
if ctx.job.max_urls_to_visit is None:
376+
return True
377+
async with self._lock:
378+
return (len(ctx.visited) + len(ctx.frontier)) < ctx.job.max_urls_to_visit
379+
362380
async def _maybe_checkpoint(self, ctx: CrawlContext) -> None:
363381
if not ctx.state_dirty:
364382
return

‎backend/models.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class CrawlJob:
3333
origin_url: str
3434
max_depth: int
3535
created_at: datetime
36+
max_urls_to_visit: int | None = None
3637
rate_limit_per_sec: float = 1.0
3738
status: JobStatus = JobStatus.PENDING
3839
error_message: Optional[str] = None
@@ -52,6 +53,7 @@ def summarize_jobs(jobs: Dict[str, CrawlJob]) -> List[Dict[str, object]]:
5253
"id": job.id,
5354
"origin_url": job.origin_url,
5455
"max_depth": job.max_depth,
56+
"max_urls_to_visit": job.max_urls_to_visit,
5557
"created_at": job.created_at.isoformat(),
5658
"updated_at": job.updated_at.isoformat(),
5759
"status": job.status.value,

‎backend/storage.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def init_db() -> None:
3535
id TEXT PRIMARY KEY,
3636
origin_url TEXT NOT NULL,
3737
max_depth INTEGER NOT NULL,
38+
max_urls_to_visit INTEGER,
3839
created_at TEXT NOT NULL,
3940
updated_at TEXT NOT NULL,
4041
status TEXT NOT NULL,
@@ -52,6 +53,7 @@ def init_db() -> None:
5253
"""
5354
)
5455
_add_column_if_missing(cur, "jobs", "updated_at", "TEXT NOT NULL DEFAULT ''")
56+
_add_column_if_missing(cur, "jobs", "max_urls_to_visit", "INTEGER")
5557
_add_column_if_missing(cur, "jobs", "rate_limit_per_sec", "REAL NOT NULL DEFAULT 1.0")
5658
_add_column_if_missing(cur, "jobs", "error_message", "TEXT")
5759
_add_column_if_missing(cur, "jobs", "processed_urls", "INTEGER NOT NULL DEFAULT 0")
@@ -168,6 +170,7 @@ def _upsert_job(cur: sqlite3.Cursor, job: CrawlJob) -> None:
168170
id,
169171
origin_url,
170172
max_depth,
173+
max_urls_to_visit,
171174
created_at,
172175
updated_at,
173176
status,
@@ -182,12 +185,13 @@ def _upsert_job(cur: sqlite3.Cursor, job: CrawlJob) -> None:
182185
active_workers,
183186
backpressure_state
184187
)
185-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
188+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
186189
""",
187190
(
188191
job.id,
189192
job.origin_url,
190193
job.max_depth,
194+
job.max_urls_to_visit,
191195
job.created_at.isoformat(),
192196
job.updated_at.isoformat(),
193197
job.status.value,
@@ -293,6 +297,7 @@ def load_jobs() -> List[CrawlJob]:
293297
id,
294298
origin_url,
295299
max_depth,
300+
max_urls_to_visit,
296301
created_at,
297302
updated_at,
298303
status,
@@ -315,6 +320,7 @@ def load_jobs() -> List[CrawlJob]:
315320
job_id,
316321
origin_url,
317322
max_depth,
323+
max_urls_to_visit,
318324
created_at,
319325
updated_at,
320326
status,
@@ -335,6 +341,7 @@ def load_jobs() -> List[CrawlJob]:
335341
origin_url=str(origin_url),
336342
max_depth=int(max_depth),
337343
created_at=datetime.fromisoformat(str(created_at)),
344+
max_urls_to_visit=int(max_urls_to_visit) if max_urls_to_visit is not None else None,
338345
status=JobStatus(str(status)),
339346
rate_limit_per_sec=float(rate_limit_per_sec or 1.0),
340347
error_message=str(error_message) if error_message is not None else None,

‎frontend/src/App.tsx‎

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ type JobSummary = {
44
id: string;
55
origin_url: string;
66
max_depth: number;
7+
max_urls_to_visit?: number | null;
78
created_at: string;
89
updated_at: string;
910
status: string;
@@ -25,6 +26,7 @@ type JobDetail = {
2526
id: string;
2627
origin_url: string;
2728
max_depth: number;
29+
max_urls_to_visit?: number | null;
2830
created_at: string;
2931
updated_at: string;
3032
status: string;
@@ -74,6 +76,7 @@ export const App: React.FC = () => {
7476
const [viewMode, setViewMode] = useState<"crawler" | "search">("crawler");
7577
const [origin, setOrigin] = useState("");
7678
const [depth, setDepth] = useState(2);
79+
const [maxUrlsToVisit, setMaxUrlsToVisit] = useState("500");
7780
const [rateLimit, setRateLimit] = useState(1.0);
7881
const [currentJobId, setCurrentJobId] = useState<string | null>(null);
7982
const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
@@ -141,7 +144,12 @@ export const App: React.FC = () => {
141144
const res = await fetch("/index", {
142145
method: "POST",
143146
headers: { "Content-Type": "application/json" },
144-
body: JSON.stringify({ origin, k: depth, rate_limit_per_sec: rateLimit })
147+
body: JSON.stringify({
148+
origin,
149+
k: depth,
150+
max_urls_to_visit: maxUrlsToVisit ? Number(maxUrlsToVisit) : null,
151+
rate_limit_per_sec: rateLimit,
152+
}),
145153
});
146154
if (!res.ok) {
147155
const data = await res.json();
@@ -219,23 +227,34 @@ export const App: React.FC = () => {
219227
};
220228

221229
const searchPanel = (
222-
<section className="panel panel-search">
223-
<h2>Search</h2>
230+
<section className="panel panel-search panel-search-upgraded">
231+
<div className="search-header">
232+
<h2>
233+
<span className="search-icon" aria-hidden="true">
234+
🔎
235+
</span>{" "}
236+
Search
237+
</h2>
238+
<p className="search-subtitle">Find relevant URLs in pages already indexed by active or finished crawlers.</p>
239+
</div>
224240
<label className="field">
225241
<span>Query</span>
226242
<input
243+
className="search-input"
227244
type="text"
228245
placeholder="search terms"
229246
value={query}
230247
onChange={(e) => setQuery(e.target.value)}
231248
/>
232249
</label>
233-
<button onClick={runSearch} disabled={!query}>
234-
Search Indexed Pages
235-
</button>
236-
<button onClick={runFeelingLucky} disabled={!query}>
237-
I'm Feeling Lucky
238-
</button>
250+
<div className="search-actions">
251+
<button onClick={runSearch} disabled={!query}>
252+
Search Indexed Pages
253+
</button>
254+
<button className="button-secondary" onClick={runFeelingLucky} disabled={!query}>
255+
I'm Feeling Lucky
256+
</button>
257+
</div>
239258
<div className="results">
240259
{results.length === 0 ? (
241260
<p className="hint">No results yet. Try searching after indexing.</p>
@@ -321,6 +340,15 @@ export const App: React.FC = () => {
321340
onChange={(e) => setDepth(Number(e.target.value))}
322341
/>
323342
</label>
343+
<label className="field">
344+
<span>Max URLs to Visit</span>
345+
<input
346+
type="number"
347+
min={1}
348+
value={maxUrlsToVisit}
349+
onChange={(e) => setMaxUrlsToVisit(e.target.value)}
350+
/>
351+
</label>
324352
<label className="field">
325353
<span>
326354
Crawler Speed (req/s)
@@ -438,6 +466,7 @@ export const App: React.FC = () => {
438466
<div className="job-card-url">{job.origin_url}</div>
439467
<div className="job-card-meta">
440468
<span>Depth: {job.max_depth}</span>
469+
<span>Max URLs: {job.max_urls_to_visit ?? "unbounded"}</span>
441470
<span>Processed: {job.processed_urls}</span>
442471
<span>Queue: {job.queued_urls}</span>
443472
<span>{new Date(job.updated_at).toLocaleTimeString()}</span>
@@ -535,6 +564,10 @@ export const App: React.FC = () => {
535564
<span className="label">Rate Limit</span>
536565
<span className="value">{selectedJob.rate_limit_per_sec.toFixed(1)} req/s</span>
537566
</div>
567+
<div className="metric">
568+
<span className="label">Max URLs</span>
569+
<span className="value">{selectedJob.max_urls_to_visit ?? "unbounded"}</span>
570+
</div>
538571
<div className="metric">
539572
<span className="label">Visited</span>
540573
<span className="value">{selectedJob.visited_count}</span>

‎frontend/src/styles.css‎

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ input {
126126
border: 1px solid rgba(148, 163, 184, 0.6);
127127
background: rgba(15, 23, 42, 0.9);
128128
color: #f9fafb;
129+
line-height: 1.25rem;
130+
font-size: 0.95rem;
129131
}
130132

131133
input:focus {
@@ -134,6 +136,12 @@ input:focus {
134136
box-shadow: 0 0 0 1px rgba(99, 102, 241, 0.4);
135137
}
136138

139+
input::placeholder {
140+
color: #94a3b8;
141+
opacity: 1;
142+
line-height: 1.25rem;
143+
}
144+
137145
button {
138146
padding: 0.5rem 0.9rem;
139147
border-radius: 999px;
@@ -145,6 +153,11 @@ button {
145153
margin-top: 0.25rem;
146154
}
147155

156+
.button-secondary {
157+
background: rgba(30, 41, 59, 0.9);
158+
border: 1px solid rgba(148, 163, 184, 0.35);
159+
}
160+
148161
button:disabled {
149162
opacity: 0.5;
150163
cursor: not-allowed;
@@ -246,14 +259,52 @@ button:disabled {
246259
min-height: 0;
247260
}
248261

262+
.panel-search-upgraded {
263+
background: linear-gradient(165deg, rgba(16, 33, 74, 0.84), rgba(9, 18, 44, 0.84));
264+
border-color: rgba(99, 102, 241, 0.25);
265+
}
266+
267+
.search-header {
268+
margin-bottom: 0.65rem;
269+
}
270+
271+
.search-header h2 {
272+
margin-bottom: 0.35rem;
273+
}
274+
275+
.search-icon {
276+
filter: drop-shadow(0 4px 8px rgba(79, 70, 229, 0.35));
277+
}
278+
279+
.search-subtitle {
280+
margin: 0;
281+
font-size: 0.9rem;
282+
color: #c7d2fe;
283+
}
284+
285+
.search-input {
286+
height: 2.6rem;
287+
}
288+
289+
.search-actions {
290+
display: flex;
291+
gap: 0.6rem;
292+
margin-bottom: 0.35rem;
293+
}
294+
295+
.search-actions button {
296+
flex: 1;
297+
margin-top: 0;
298+
}
299+
249300
.panel-job-detail {
250301
margin-bottom: 1rem;
251302
}
252303

253304
.results {
254305
margin-top: 0.75rem;
255306
flex: 1;
256-
min-height: 0;
307+
min-height: 140px;
257308
overflow: auto;
258309
border: 1px solid rgba(148, 163, 184, 0.18);
259310
border-radius: 0.75rem;
@@ -476,5 +527,9 @@ td a {
476527
width: 100%;
477528
justify-content: center;
478529
}
530+
531+
.search-actions {
532+
flex-direction: column;
533+
}
479534
}
480535

0 commit comments

Comments
 (0)