Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Files, Paths & Encodings  ›  Capstone Project

Capstone — The Yard

40 minFind, sniff and decode everything in a directory — and trust the bytes over the name
You're building a piece ofQuarry — the yard
This piece — open_source(): Finds, sniffs and decodes everything in a directory, and says which files lied.
Scenario Quarry is pointed at the real shared folder: logs, a Windows export, a UTF-16 spreadsheet, the pump telemetry — and one sensor file somebody renamed to .log. The yard has to get every one of them right.
Your task
Wire Quarry's yard together. The finder, decoder and header reader from earlier lessons are provided. Write sniff(data), which returns (kind, encoding, count) — a telemetry header wins first, then a byte-order mark means text, then a NUL byte means binary, otherwise decode as text and count lines — and open_source(files), which plants the files, sniffs every source, and returns {path: {"kind", "encoding", "count", "lied"}}. A file lied if a text suffix holds non-text, or a .qry holds something that isn't telemetry. Example: a .log whose first bytes are QRY1 → {"kind": "telemetry", "encoding": "binary", "count": 12, "lied": true}.

Capstone — The Yard

In short: this capstone wires lessons 01–05 into the yard — one pipeline that takes a directory and reports, for every source file, what it really is. Every file is read once, as bytes (lesson 02); sniff weighs the evidence strongest-first — a telemetry magic (lesson 05), then a byte-order mark, then a NUL byte, then everything else as text, decoded by lesson 04's rules — and only text gets decoded, only once, at the edge. The section's one idea pays off here: a filename is a claim; the bytes are evidence. When they disagree, the bytes win, and the disagreement gets recorded instead of silently ignored.

A pipeline: find_sources (pathlib) feeds read_bytes (binary, once) into sniff, which branches on the strongest evidence first — telemetry magic, then a BOM, then a NUL byte, then plain text — and only the text branch calls decode

Weigh the evidence before deciding what a file is

Here is the real shared folder Quarry has been pointed at: a server log, a Windows export, a UTF-16 spreadsheet, the hourly pump telemetry, a hidden editor cache, a README — and one file somebody renamed. It says odd.log. Its first four bytes say QRY1.

The obvious-looking rule, "a NUL byte means binary", is wrong on its own:

What does this print?
data = "id,name\n".encode("utf-16")
label = "binary" if b"\x00" in data else "text"
print(label)

So sniff(data) weighs the evidence strongest-first:

  1. The telemetry magic. A valid QRY1 header with a record count is about as unambiguous as evidence gets. If header_records(data) returns a number, the file is telemetry — whatever its name says.
  2. A byte-order mark. EF BB BF, FF FE or FE FF at the start was put there on purpose by the program that wrote the text. It means text, and it names the encoding.
  3. A NUL byte, now that a BOM has been ruled out, means this isn't text in any encoding the yard handles — binary of some kind nobody told Quarry about.
  4. Everything else is text, decoded by lesson 04's rules and counted with splitlines(), which handles \n, \r\n and \r alike.
step through it
1import codecs
2 
3samples = [b"QRY1....", "hi".encode("utf-16"), b"\x01\x00\x02", b"ok\r\nfine"]
4labels = []
5for data in samples:
6 if data.startswith(b"QRY1"):
7 label = "telemetry"
8 elif data.startswith((codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE, codecs.BOM_UTF8)):
9 label = "text (bom)"
10 elif b"\x00" in data:
11 label = "binary"
12 else:
13 label = "text"
14 labels.append(label)
15print(labels)

(The real sniff is stricter about telemetry than startswith(b"QRY1") — it insists on a full 20-byte header, so a four-byte fragment isn't mistaken for a sensor file.)

Catch the sensor file wearing a .log costume

The yard compares the suffix's claim against the bytes' evidence. Most of the time they agree. When they don't, the bytes win, and the mismatch is recorded — because it usually means something upstream broke:

What does this print?
from pathlib import PurePosixPath

name = "sensors/odd.log"
data = bytes.fromhex("51525931010000") + bytes(13)
print(PurePosixPath(name).suffix, data[:4] == b"QRY1")

Here's the naive version — decode everything as UTF-8 and count lines. It works on the honest file and falls over on the renamed one. Fix it so telemetry is recognised before anything tries to decode it:

broken — fix itUnicodeDecodeError

Run it and read which byte UTF-8 rejected. Then sniff first and only decode what is text.

The rule for lied is short once sniff exists: a text suffix (.log, .csv, .json, .txt) lies if the content isn't text; a .qry lies if the content isn't telemetry. A UTF-16 CSV does not lie — it's text in an unusual encoding, and the encoding column already says so.

Read every file once, in binary

A detail that's easy to get wrong: the yard reads every file with path.read_bytes() — binary mode, once. It never opens a file in text mode "to have a look", for three reasons you already know: text mode needs an encoding, and choosing it is the question being answered; text mode translates line endings, destroying the evidence lesson 02 cared about; and a telemetry file isn't text at all, so a text-mode read would either crash or, worse, succeed with nonsense.

python

Once sniff has decided, decode(data) turns bytes into a str exactly once, at the edge. Everything downstream of the yard — parsers, records, a database in a later section — only ever sees text or typed records, never raw bytes. An encoding bug in Quarry can only ever live in that one place.

Here's the whole yard, drawn as the pieces it's built from:

Drawing diagram…

What you're given, and what you write

The starter already contains, compacted, the pieces from earlier lessons: plant(root, files), find_sources(root), decode(data) and header_records(data). You write two functions:

  1. sniff(data) → (kind, encoding, count), weighing the evidence in the order above. Telemetry is ("telemetry", "binary", records); unknown binary is ("binary", "binary", 0); text is ("text", encoding, number of lines).
  2. open_source(files) → plant the yard, loop find_sources(root), sniff each file's bytes, and build the report keyed by path.relative_to(root).as_posix(), adding "lied" from the suffix and the kind.

Quick practice

What does this print?
import struct

HEADER = struct.Struct("<4sBBHI8s")
data = b"QRY1" + b"\x01"
print(len(data) < HEADER.size)
What does this print?
import codecs

def sniff_simple(data):
    if data.startswith(b"QRY1"):
        return "telemetry"
    if data.startswith((codecs.BOM_UTF8, codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE)):
        return "text (bom)"
    if b"\x00" in data:
        return "binary"
    return "text"

print(sniff_simple(b"boot ok\nwarm\n"))
broken — fix itUnicodeDecodeError

Run it and read which byte UTF-8 rejected. Sniff before you decode.

What does this print?
suffix, kind = ".csv", "text"
TEXT_SUFFIXES = {".log", ".csv", ".json", ".txt"}

def lied(suffix, kind):
    if suffix in TEXT_SUFFIXES:
        return kind != "text"
    if suffix == ".qry":
        return kind != "telemetry"
    return False

print(lied(suffix, kind))

🎯 Your turn

Build sniff(data) and open_source(files):

  • a .log whose bytes start with a QRY1 header recording 12 readings → {"kind": "telemetry", "encoding": "binary", "count": 12, "lied": True}
  • a UTF-16 .csv with two lines → {"kind": "text", "encoding": "utf-16", "count": 2, "lied": False}

Hint — telemetry first, then a BOM, then a NUL byte, then decode; read each file once with read_bytes(); decide lied from path.suffix.lower() and the kind.

Then press ▶ Run and hit ✓ Check. ✅

Practice — level up

0/6 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upWhat the name claims

Write claimed_kind(name) returning what a filename claims to be: .log → "log", .csv → "csv", .json → "json", .txt → "text", .qry → "telemetry", anything else "unknown". Ignore case, and look through rotation numbers: app.log.3 is a log.

filesystem-pathspath-suffix
DrillHidden, anywhere in the path

Write is_hidden(path): True if any part of the POSIX path starts with a dot. is_hidden("srv/.cache/x.log") → True.

filesystem-paths
BuildDraw the badged tree

Write badge_tree(badges) taking {posix path: label} and returning the yard as indented lines: folders as name/, files as name [label], two spaces per level, everything in sorted path order, each folder printed once before its contents.

filesystem-pathssorted
BossWhich files lied?

Write find_liars(files) taking {posix path: hex} and returning the sorted paths whose content contradicts their suffix: a text suffix (.log .csv .json .txt) holding a QRY1 magic or a NUL byte without a UTF-16 BOM, or a .qry file not starting with QRY1. Other suffixes never lie.

binary-datafilesystem-paths
CapstoneThe yard's summary line

Using the full pipeline (it's in the starter), write yard_report(files) returning totals across every source: {"sources": n, "text_lines": total lines of text files, "telemetry_records": total records, "encodings": {encoding: count of text files}, "liars": number that lied}.

text-encodingfile-iofilesystem-paths
ExplainTrust the bytes, decode at the edge

Quarry decides what a file is from its first bytes, not its name, and turns bytes into text in exactly one place — the yard — before anything else sees them. Explain why each of those two rules is worth its cost.

open_source.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
open_source(files) → dictEvery source in the yard, badged with what it really is.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.