Penge ng slot? Tingnan muna natin.
Read-only, rate-limited DFA passport appointment availability for the Philippines, in Python.
It looks; it never books.
Unofficial. Not affiliated with, endorsed by or connected to the Department of Foreign Affairs of the Philippines. Passport appointments are free and are booked only at passport.gov.ph.
PengePassportPH tells you which dates, and which hourly time slots, still have room at a DFA consular site. It reads the same data the booking page shows, through the same endpoints. It paces itself so it cannot overload the site, and it never selects a time slot: selecting one holds it for 30 minutes and takes it away from real applicants.
This is the Python package. It is a native port of the Node.js package with the same behaviour, the same CLI and one shared rate limit: Python and Node.js processes on the same machine count against a single budget.
- Standard library only. No runtime dependencies. Python 3.11+, fully typed (
py.typed). - Synchronous and thread-safe. Share one client between threads; identical concurrent calls share one request.
- Polite by default, and it can't be made faster. At least 2 s between requests, a capped hourly budget, backoff when the site struggles.
Install
pip install penge-passport-ph # or: uv add penge-passport-ph
pipx install penge-passport-ph # just the CLI: penge-passport-ph, or penge
CLI
Same commands, flags, output and exit codes as the Node.js CLI:
$ penge sites --search antipolo
486 Antipolo (SM Center, Antipolo City, Rizal)
$ penge check --site antipolo --times
Antipolo (SM Center, Antipolo City, Rizal) (site 486)
Checked 2026-09-26 to 2027-03-31 for 1 applicant(s)
Earliest: 2026-10-14
Available (11): 2026-10-14, 2026-10-15, 2026-10-16, ...
$ penge watch --site 486 --site angeles --interval 10m
penge --help lists everything; python -m penge_passport_ph works too.
Library
from penge_passport_ph import PengePassportPH
penge = PengePassportPH(contact="you@example.com")
site = penge.find_sites("antipolo")[0]
availability = penge.availability(site.id)
print(availability.earliest) # '2026-10-14', or None when fully booked
print(availability.available_dates) # ('2026-10-14', '2026-10-15', …)
if availability.earliest:
for slot in penge.time_slots(site.id, availability.earliest):
print(slot.start, slot.end, slot.status) # 08:30 09:30 Available Slots: 1
for event in penge.watch([486, 10], interval=600):
if event.type == "availability" and event.opened:
print(event.site_id, "opened", event.opened)
| Method | Returns |
|---|---|
regions() |
The five regions. |
countries(region_id) |
list[Country] |
sites(region_id=1, country_id=1) |
list[Site], the Philippines by default |
find_sites(text, region_id=1, country_id=1) |
Sites whose name or description contains text |
availability(site_id, *, applicants=1, from_date=None, to_date=None) |
Availability: earliest, available_dates, and every published days entry |
time_slots(site_id, date, *, applicants=1) |
list[TimeSlot] for one date |
watch(site_ids, *, interval=300, applicants=None, stop=None) |
Iterator of AvailabilityEvent and ErrorEvent; set the threading.Event stop, or break, to end it |
stats() |
The rate limiter's state: requests in the last hour, failures, pauses in seconds |
Results are frozen dataclasses; to_dict() gives the same JSON as the Node.js package. Dates are
YYYY-MM-DD strings. applicants is 1, or 2 to 5 for a group (MAX_APPLICANTS, the most the DFA's group
form takes). from_date and to_date default to the site's own booking window. A date absent
from days is not published yet, which is different from fully booked.
Errors all derive from PengePassportPHError: UpstreamError (unexpected status or body, or the site
unreachable, with .status), SessionError, RateLimitError (with .retry_after in seconds) and its
subclass CircuitOpenError. Invalid arguments raise ValueError.
Rate limits
| Limit | Default | Allowed |
|---|---|---|
Gap between requests (min_interval) |
3 s, plus up to 25% jitter | 2 s or more |
Requests per rolling hour (max_requests_per_hour) |
300 | 1 to 1200 |
| Concurrency | 1 | fixed |
Reuse of availability and slot answers (availability_ttl) |
60 s | 30 s or more |
watch interval |
5 min | 60 s or more, within the hourly budget |
The limiter's state lives in ~/.local/state/penge-passport-ph/ (override with state_dir or
PENGE_PASSPORT_PH_STATE_DIR), in the same file and format as the Node.js package, behind the same lock.
Any non-2xx response, unreadable body, timeout or network error pauses the client, doubling up to
10 minutes; it honours Retry-After, and after 5 failures in a row it rests for 15 minutes. If the state
file can't be written, it issues a StateSharingWarning and limits the current process only.
Please don't run it on many machines, don't use it to resell or broker appointments, and don't build booking on top of it.
Configuration
PengePassportPH(
base_url="https://passport.gov.ph",
contact=None, # added to the User-Agent
min_interval=3.0, # seconds, at least 2
max_requests_per_hour=300, # at most 1200
max_wait=60.0, # refuse, rather than queue, past this
availability_ttl=60.0, # at least 30
directory_ttl=6 * 3600,
timeout=20.0,
state_dir=None, # default: default_state_dir()
transport=None, # custom HTTP transport (proxies, tests)
)
More
- How it works: the endpoints, the site's quirks, and the shared state-file protocol.
- Canary runbook: the scheduled check that fails loudly when passport.gov.ph changes. It covers this package too.
- Law and responsible use: the DFA's rules, Philippine law, and why the limits are what they are.
- Changelog
MIT © Alpha Romer Coma
Release files for penge-passport-ph 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| penge_passport_ph-0.1.0.tar.gz | 32.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| penge_passport_ph-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 70.3 kB
Release files / penge_passport_ph-0.1.0.tar.gz
| Download URL | penge_passport_ph-0.1.0.tar.gz |
|---|---|
| Size | 32.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
71445b43e02a896d6e5dd33989b7eb50fbb732bd6456d994938ef6dae97af8e0
|
|
BLAKE2b-256 checksum How to use checksums |
7e2d11711eaed3fb8d5080ddcfae449c701621ccc60a7740578494fd5c231400
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / penge_passport_ph-0.1.0-py3-none-any.whl
| Download URL | penge_passport_ph-0.1.0-py3-none-any.whl |
|---|---|
| Size | 38.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a4237c9d309398f936b2e3215d604a381c1a8684fa8bed15d21762e569064339
|
|
BLAKE2b-256 checksum How to use checksums |
738b46b494f224f39bc9928092106d2ab27c102b2e4a37546e37c7798ae4d882
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|