API

Rajasthan land records (jamabandi) via Bhu-Naksha.

Khatedar name, father, jati and residence per plot.

Thin client for the Rajasthan Bhu-Naksha portal.

The portal is the map front-end to the state’s land records. Its REST layer, unlike the Apna Khata nakal page, asks for no login, no OTP and no captcha: the “captcha” on the map’s Nakal button is generated and checked in browser JavaScript and is never sent to the server.

Two calls matter.

ListsAfterLevel walks a six-level tree: district, tehsil, RI circle, halka, village, sheet. Codes are passed back as a comma-separated string with a trailing comma, exactly as the page builds it; without the trailing comma the call returns empty lists.

getPlotInfo takes a giscode, which is the six level codes concatenated (01 + 002 + 0745 + 02920 + 11035 + 001), and a plot number, and returns one JSON object whose info field holds the khata number and one line per co-owner: name, father or husband, share, जाति- and residence. A plot that does not exist answers with an empty body and HTTP 200 (occasionally 204), not an error, so “miss” is a first-class outcome here.

exception rajasthan_ror.portal.PortalError[source]

The portal answered, but not with what the call asked for.

class rajasthan_ror.portal.Level(code, name, has_data)[source]

One entry in the portal’s location tree.

Parameters:
code

The portal’s code for this entry, zero-padded as it sends it.

Type:

str

name

The Devanagari name shown in the map’s drop-down.

Type:

str

has_data

Whether the portal reports a map (and so plot records) beneath this entry.

Type:

bool

rajasthan_ror.portal.with_connection_backoff(call, *, what, sleep=<built-in function sleep>, total=7200.0)[source]

Run call until it stops failing at the connection level.

Name resolution, connect and read failures are retried with a delay that doubles from CONNECT_BACKOFF_START to CONNECT_BACKOFF_CAP for up to total seconds. Anything the portal actually answers is not a connection failure and is returned to the caller as is.

Parameters:
  • call (Callable[[], T]) – The request to make.

  • what (str) – Label for the log.

  • sleep (Callable[[float], None]) – Sleep function; tests pass a recorder.

  • total (float) – Seconds of waiting after which to give up.

Returns:

Whatever call returns once it succeeds.

Raises:

PortalError – When total seconds of retrying did not get through.

Return type:

T

class rajasthan_ror.portal.Session(pause=0.2, timeout=60.0, retries=4)[source]

One HTTP session against the portal, with bounded retries.

The portal has shown no 429s, but latency swings from under a second to several, and a crawl that hammers it is a crawl that gets blocked. A connection-level failure is waited out (see with_connection_backoff()); an answer the portal gives that is not what was asked for is retried retries times and then raised.

Parameters:
children(codes)[source]

List the entries one level below codes.

Parameters:

codes (list[str]) – Level codes from the district down; empty for the districts themselves.

Returns:

The child entries in the portal’s order.

Raises:
  • ValueError – When codes already reaches the sheet level.

  • PortalError – When the portal’s answer is not a non-empty list.

Return type:

list[Level]

plot_info(giscode, plotno)[source]

Fetch the record for one plot.

Parameters:
  • giscode (str) – The six level codes concatenated; see giscode().

  • plotno (str) – Plot (khasra) number as printed on the map.

Returns:

The portal’s JSON object, or None when the portal says there is no such plot.

Raises:

PortalError – When the body is neither empty nor JSON.

Return type:

dict[str, Any] | None

extent(giscode)[source]

Fetch the georeferenced bounding box of one sheet.

Parameters:

giscode (str) – The six level codes concatenated.

Returns:

The portal’s JSON object describing the extent.

Return type:

dict[str, Any]

rajasthan_ror.portal.giscode(codes)[source]

Build the sheet key getPlotInfo wants from the six level codes.

Parameters:

codes (list[str]) – District, tehsil, RI circle, halka, village and sheet codes.

Returns:

The codes concatenated in that order.

Raises:

ValueError – When there are not exactly six codes.

Return type:

str

Enumerate the portal’s location tree down to the sheet, one row per sheet.

Six levels: district, tehsil, RI circle, halka, village, sheet. The sheet is the unit getPlotInfo is keyed on, so it is the unit the crawl is keyed on. Villages typically have one sheet; a village listed twice, as (गत) past and (चालु) current, carries one code and is folded to the current row.

Checkpointed per district as gzipped JSONL under raw/locations/ so an interrupted run resumes at the district it was in, and folded into raw/villages.parquet at the end. raw/ is under the current working directory; see rajasthan_ror.paths.

Usage:

uv run rajasthan-ror-list [–districts 01,21] [–workers 4]

rajasthan_ror.locations.walk(session, codes, names, sheets)[source]

Descend from codes to the sheets beneath it, appending one row each.

Parameters:
  • session (Session) – Portal session to query.

  • codes (list[str]) – Level codes of the entry to descend from.

  • names (list[str]) – Level names matching codes.

  • sheets (list[dict[str, Any]]) – Output list; a row per sheet carries every level’s code and name, the giscode and the has_data flag.

Return type:

None

rajasthan_ror.locations.district_file(code)[source]

Checkpoint file for one district.

Parameters:

code (str) – District code as the portal gives it.

Returns:

Path of the gzipped JSONL under raw/locations/.

Return type:

Path

rajasthan_ror.locations.list_district(code, name)[source]

List every sheet in one district and checkpoint the result.

Parameters:
  • code (str) – District code.

  • name (str) – District name, for the log and the rows.

Returns:

Number of sheets written; zero when the district was already listed.

Return type:

int

rajasthan_ror.locations.fold()[source]

Combine the district checkpoints into raw/villages.parquet.

Returns:

One row per sheet, past/current village pairs folded to the current row.

Return type:

DataFrame

rajasthan_ror.locations.main()[source]

Command-line entry point: list districts in parallel, then fold.

Return type:

None

Fetch every plot record for every sheet in raw/villages.parquet.

The portal has no call that lists the plots on a sheet; the map finds them by click. So the crawl walks plot numbers upward from 1. Khasra numbers are dense integers, but subdivided khasras are written 1256/287 on the nakal and the portal has not answered to that form under any spelling tried, so what this collects is the integer-numbered plots. The parse records which numbers were tried.

Where a sheet ends is found by probing, not by a long run of misses. Over 68 finished Nagaur sheets the largest gap inside a sheet’s numbering had median 3 and maximum 57, so a plain miss run had to be 60 to be safe and cost 60 requests on every sheet, 16% of all requests. Instead, after --probe-after consecutive misses the crawl probes ahead at doubling offsets (20, 40, 80, 160, 320, 640 past the current number). A hit resumes the walk; six misses end the sheet. That is 26 requests where 60 were, and it reaches 660 numbers past the last hit where the old rule reached 60.

Every hit carries ownerplots, the other plot numbers on the same khata. Those share the owner block by construction, so they are recorded as via the plot that named them rather than fetched, which cuts the request count from the number of plots to roughly the number of khatas plus the misses. The parse expands them back to one row per plot.

Checkpointing is per sheet, appended to raw/plots/<giscode>.jsonl.gz, one line per plot number tried: ok true with the portal’s JSON, ok false with a reason. A sheet whose file ends with a done line is skipped on the next run; one that does not replays saved answers and fetches missing numbers. raw/ is under the current working directory; see rajasthan_ror.paths.

Usage:

uv run rajasthan-ror-fetch –districts 21 –workers 8 uv run rajasthan-ror-fetch –giscodes 0100207450292011035001

rajasthan_ror.plots.sheet_file(giscode)[source]

Checkpoint file for one sheet.

Parameters:

giscode (str) – The sheet’s giscode.

Returns:

Path of the gzipped JSONL under raw/plots/.

Return type:

Path

rajasthan_ror.plots.trim_truncated(path)[source]

Cut a checkpoint with a corrupt tail back to its readable prefix.

gzip readers stop at the first damaged member, so appending to a file a crash cut short would hide every later record from the parser. The original bytes are backed up before the file is rewritten with its readable lines; the crawl then refetches from there.

Parameters:

path (Path) – The sheet’s checkpoint file; may not exist yet.

Returns:

Number of lines kept, or zero when the file was intact or absent.

Return type:

int

rajasthan_ror.plots.read_progress(path)[source]

Recover where a sheet’s crawl got to from its checkpoint file.

Tolerates a truncated tail from a killed worker: the readable prefix is used and the rest is refetched. trim_truncated should run first so the tail is not appended after.

Parameters:

path (Path) – The sheet’s checkpoint file; may not exist yet.

Returns:

Highest plot number tried, whether the sheet is finished, the number of hits so far, and the plot numbers already known via ownerplots, each mapped to the plot that named it.

Return type:

tuple[int, bool, int, dict[str, str]]

rajasthan_ror.plots.integer_plots(ownerplots)[source]

Plot numbers the walk could reach, i.e. plain integers.

Slashed (subdivided) numbers are dropped: the portal does not answer to them. The portal sends ownerplots sometimes as a list and sometimes as its string repr, so both shapes are read.

Parameters:

ownerplots (str | list[Any] | None) – The ownerplots field of a plot record.

Returns:

The integer plot numbers, as strings.

Return type:

set[str]

rajasthan_ror.plots.crawl_sheet(session, giscode, probe_after, max_plot)[source]

Walk one sheet’s plot numbers upward, probing ahead to find its end.

A portal failure is checkpointed before it propagates to the caller.

Parameters:
  • session (Session) – Portal session to fetch with.

  • giscode (str) – The sheet to crawl.

  • probe_after (int) – Consecutive misses after which to probe ahead.

  • max_plot (int) – Highest plot number to try.

Returns:

Requests made and plots found (including ones already known from a previous run).

Return type:

tuple[int, int]

rajasthan_ror.plots.run()[source]

Crawl the queued sheets with a worker pool.

Raises:

SystemExit – With status 1 if any sheet failed, allowing a supervisor to retry the unfinished pass.

Return type:

None

rajasthan_ror.plots.main()[source]

Run one fetcher at a time so checkpoint writers cannot overlap.

Raises:

SystemExit – With status 1 for unfinished work or 2 for an active writer.

Return type:

None

Split fetched plot records into one row per khatedar.

Reads only raw/plots/ and writes raw/owners.parquet (both under the current working directory; see rajasthan_ror.paths), so a parser change never costs a refetch.

The info field is a short free-text block:

क्षेत्रफल : 0.0600 Hectare खाता संख्या : 847 1.) गुलाब सिंह चीता पुत्र अमर सिंह हिस्सा- 2/3 जाति- मेर(मेहरात, चीता) सा. अजयसर खातेदार 2.) सुगरा पत्नि खंगार सिंह हिस्सा- 1/3 जाति- मेर(मेहरात, चीता) सा देह खातेदार

Each numbered entry is one co-owner: a name, then a relation word (पुत्र son of, पत्नि wife of, …) and the relative, then हिस्सा- the share, जाति- the caste, a residence introduced by सा. (sakin, resident of; देह means this very village) or निवासी or an urban address block starting मकान संख्या-, and a closing tenure word such as खातेदार.

The parse is a marker scan, not one whole-line regex, for the reason the Odisha parser gives: a single regex silently drops every shape it did not anticipate, and the shapes here already include institutional owners with no relation and no caste, a caste with a parenthesised, comma-bearing gloss, and a residence that is an address rather than a village. Every row keeps the raw line so a parse can be audited against its source. An entry can wrap across embedded newlines; its continuation belongs to the same owner.

Usage:

uv run rajasthan-ror-parse

rajasthan_ror.parse.split_owner(line)[source]

Split one numbered owner block into fields.

A missing marker yields None for that field, never a dropped row.

Parameters:

line (str) – One N.) ... entry, including any embedded line breaks.

Returns:

owner_seq, raw_line, name, relation, relative, share, jati, residence and tenure.

Raises:

ValueError – When the line does not start with an owner number.

Return type:

dict[str, Any]

rajasthan_ror.parse.split_info(info)[source]

Split an info block into area, khata number and owner rows.

Parameters:

info (str) – The free-text block from a plot record.

Returns:

area_ha (float or None), khata (str or None) and owners, one split_owner() dict per numbered line or explicit unnumbered government owner (whose owner_seq is None).

Return type:

dict[str, Any]

rajasthan_ror.parse.rows_from_record(record, by_plot=None)[source]

Owner rows for one fetched record.

A via record, one known only from another plot’s ownerplots, borrows that plot’s data from by_plot.

Parameters:
Returns:

One row per co-owner, each carrying the plot’s giscode, plot number, khata, area, owner count and via source; empty for a miss.

Return type:

list[dict[str, Any]]

rajasthan_ror.parse.main()[source]

Command-line entry point: parse every sheet into raw/owners.parquet.

Return type:

None

Map a jati string as the patwari wrote it to a Rajasthan schedule category.

The schedules (schedules/rajasthan_schedules.json, shipped with the package) are in Roman script, as the Social Justice department publishes them; the records are in Devanagari, spelled by hand. Matching goes through a phonetic key: Devanagari is transliterated (Harvard-Kyoto), the inherent vowel and aspiration are dropped, long and short vowels merged, w and v merged, doubled letters collapsed. मेघवाल and Meghwal both key to megvl; जाट and Jat to jt; Jatia stays jti.

A record’s caste field can name several communities at once (मेर(मेहरात काठात,मेहरात-घोड़ात, चीता)) or carry a religion (लोहार मुसलमान). The string is split on brackets, commas and hyphens and every piece is tried; the first schedule hit wins, in the order SC, ST, MBC, OBC, because a community that appears in two lists is in the earlier one by a narrower definition (Dholi Bhil is ST, Dholi is SC). A string that hits no schedule is labelled unlisted, which is where Rajput, Brahmin, Bania and Jain sit, and is not the same as General: an unlisted string may be a misspelling the key did not bridge.

Usage as a library: categorise("मेघवाल") -> ("SC", "Megh, Meghval, Meghwal, Menghvar", "exact").

class rajasthan_ror.categorise.ScheduleEntry[source]

One synonym row of a schedule.

category

SC, ST, MBC or OBC.

Type:

str

entry_no

The entry’s serial number in that schedule.

Type:

int

entry

The entry as published, all its names in one string.

Type:

str

synonym

One name from entry, the unit matching works on.

Type:

str

source

Where the schedule was taken from.

Type:

str

rajasthan_ror.categorise.key(text)[source]

Phonetic key shared by a Devanagari spelling and its Roman schedule entry.

The inherent vowel a goes (it is what Roman spellings drop: jATa and Jat); the other vowels stay, with long and short merged, because without them Jat and Jatia or Bhil and Balai collide.

Parameters:

text (str) – A caste name in Devanagari or Roman script.

Returns:

The key; empty when nothing alphabetic survives.

Return type:

str

rajasthan_ror.categorise.schedule_entries()[source]

Load and validate the packaged schedules.

Returns:

Every synonym row, in file order.

Raises:

ValueError – When the file’s schema version is not SCHEMA_VERSION, an entry fails _validated_entry(), or a (category, entry_no, synonym) triple repeats.

Return type:

tuple[ScheduleEntry, …]

rajasthan_ror.categorise.schedules()[source]

Index the schedules by phonetic key.

Returns:

Key to [(category, entry)], each list in schedule order so the narrowest listing comes first.

Return type:

dict[str, list[tuple[str, str]]]

rajasthan_ror.categorise.pieces(jati)[source]

Split a caste string into the community names it carries.

Parameters:

jati (str) – The caste field as written.

Returns:

Non-empty pieces, split on brackets, commas, slashes, hyphens and the Hindi conjunctions.

Return type:

list[str]

rajasthan_ror.categorise.categorise(jati, fuzzy_cutoff=0.9)[source]

Look up one caste string in the schedules.

Parameters:
  • jati (str | None) – The caste field as written; None or blank is unlisted.

  • fuzzy_cutoff (float) – difflib.get_close_matches() ratio a key must reach to count as a near miss. Keys shorter than five letters are never fuzzy-matched.

Returns:

Category (SC, ST, MBC, OBC or unlisted), the schedule entry matched or None, and how: exact for a key match, fuzzy for a close key, none when nothing in the schedules is near it.

Return type:

tuple[str, str | None, str]

Where the crawl keeps its files.

Everything lives under raw/ in the current working directory, not next to the package: the package is installed into a virtualenv, while the crawl is run from the repository root (crawl.sh does cd there first) and its checkpoints must survive reinstalls. Run the console scripts from the directory that should hold raw/.