Skip to main content

English · Русский

monokeys

A client for Keys — small smart functions, each callable in one line. Standard library only: no dependencies, one file.

pip install monokeys
from monokeys import Keys

k = Keys()                                    # nothing to configure

print(k.alive.text("@durov"))
# alive · channel · Pavel Durov · 10 998 851 subscribers

print(k.answer.text("weather in Haifa"))
# Haifa: 27.4°C, wind 8.0 m/s, humidity 68% · source: weather

No signup, no access key, no quota: the client picks up the public key itself, and that key never expires and is never metered.

alive — is this Telegram link alive?

No account, no Bot API, no token: it reads the public t.me preview page — the same one messengers use to draw a link card.

k.alive("@durov")                  # the whole answer as an object
k.alive.members_count("@durov")    # 11005185 — just the number, already an int
k.alive.kind("ru_python")          # group
k.alive.verified("@BotFather")     # True

Understands every shape a link comes in: @durov, t.me/durov, t.me/durov/123, t.me/s/durov, t.me/+AbCdEf…, t.me/joinchat/…, DUROV.

Fields: is_alive, kind (channel / group / bot / user), title, description, members_count, members_label, online_count, verified, is_private, needs_request, restricted, has_preview, avatar_url, deep_link, action, username, url, error.

The subtlety that makes naive checks lie: Telegram answers 200 for a deleted channel and renders a placeholder. Alive and dead differ in the markup, not in the status code.

answer — one question, many sources at once

k.answer.text("who is Pavel Durov")   # Pavel Valeryevich Durov is a technology…
k.answer.text("weather in Haifa")     # Haifa: 27.4°C, wind 8.0 m/s
k.answer.text("100 usd to eur")       # 100 USD = 86.23 EUR · rate 0.86
k.answer("Haifa", sources="osm,wiki") # only the map and Wikipedia

The question goes to several places in parallel, each answers in its own field, and the best one is named separately along with links you can check it against. Leave sources out and the key reads the question and decides for itself.

Thirteen sources, not a single paid key: Wikipedia, Wikidata, Wiktionary, DuckDuckGo, OpenStreetMap, Open-Meteo, exchange rates, Crossref, arXiv, Open Library, PyPI, npm, GitHub.

Ask exactly the source you want

Every source is a method of its own. Same word, different answers:

k.answer.weather("Haifa")   # Haifa: 27.4°C, wind 8.0 m/s, humidity 68%
k.answer.osm("Haifa")       # Haifa, Haifa Subdistrict, Haifa District, Israel
k.answer.wiki("Haifa")      # Haifa is the third-largest city in Israel…
k.answer.rates("100 usd to eur")    # 100 USD = 86.23 EUR
k.answer.github("telegram")         # DrKLO/Telegram ★29798

Exchange rates that do the math

k.answer.rates("100 dollars to shekels")   # 100 USD = 299.24 ILS · rate 2.99
k.answer.rates("50 eur to ils and usd")    # 50 EUR = 173.06 ILS, 58.22 USD
k.answer.rates("100 UZS to KGS")           # codes work for all 166 currencies

The amount, the currencies and their order are parsed out of the question. 166 currencies, including ones the European Central Bank does not publish.

Two languages

The language is detected from the question itself: Cyrillic goes to Russian sources, Latin to English ones.

dev — a vulnerability, a domain or an address

k.dev.text("CVE-2021-44228")
# cve CVE-2021-44228 · critical · Apache Log4j2 2.0-beta9 through 2.15.0 …

k.dev.text("github.com")
# domain github.com · MarkMonitor Inc. · expires 2026-10-09 · 140.82.121.3

k.dev.text("8.8.8.8")
# ip 8.8.8.8 · Ashburn United States · Google LLC

The key works out what you gave it: a CVE number, a domain and an IP address look nothing like each other. Three things a developer looks up constantly, opening three different sites every time.

Open sources, no keys: CIRCL (a CVE database mirror), RDAP instead of the retired whois, DNS over Cloudflare, the Wayback Machine, ip-api.

crypto — what a coin costs

k.crypto.text("bitcoin")     # Bitcoin · 78 293 USD · 24h +0.08% · rank 1
k.crypto("ETH", vs="usd")

Deliberately separate from ordinary exchange rates: a currency has one central bank number per day, a coin has a price in several currencies at once, a 24-hour move and a market cap — and all of it changes by the minute.

time — what time it is there, and what time yours is there

k.time('Haifa')
# Haifa: 00:15, 08.09.2026 · Tuesday · Asia/Jerusalem

k.time('Haifa', to='New York', at='15:00')
# Haifa: 15:00, 08.09.2026 · Tuesday · Asia/Jerusalem · New York: 08:00 · 7 h behind

A question about time almost never ends at «what time is it». The real one is «my call is at 15:00, what is that for them» — and you do that arithmetic by hand every single time. Daylight saving is already accounted for, and if the answer lands on the next day there, the key says so:

k.time('Haifa', to='Tokyo', at='23:00', only='to_time,next_day')
# '05:00 · True'

weather — weather in words

k.weather('Haifa')
# Haifa: partly cloudy · 27.6°C ≈32.8°C · 27.2°C…31.4°C · ☔0%

k.weather('Berlin', when='tomorrow', only='description,umbrella')
# 'overcast · True'

Numbers answer the wrong question. People ask «do I need an umbrella» and get code 61 and 83% humidity. Here it is the other way round: weather in words, apparent temperature next to the real one, chance of rain and a straight answer about the umbrella. Forecast up to seven days. «Tomorrow» is counted in the place's own timezone, not our server's.

read — a page as text

article = k.read('https://peps.python.org/pep-0008/')
article.text        # 'Indentation\\n\\nUse 4 spaces per indentation level...'
article.words       # 1010
article.title       # 'PEP 8 – Style Guide for Python Code'
article.published   # '2001-07-05'

118 KB of markup become 1010 words of text. Menus, banners, the footer and the «read next» block stay out. The encoding is taken from the page itself — plenty of sites still ship cp1251, and trusting the HTTP header alone gives you mojibake.

When there is no connected text, the key says so instead of passing a list of links off as an article:

k.read('https://news.ycombinator.com/', only='is_article')   # False

url — a link card

k.url('bit.ly/3xYz')
# url:         'https://example.com/article'   ← where it actually leads
# title:       'Page title'
# image:       'https://example.com/og.png'
# alive:       True
# redirected:  True

The same thing a messenger shows when you paste a link into a chat. Separate from read on purpose: the reader needs text and pays for it by parsing the whole page, while a card only needs the first few kilobytes.

email — address validation

k.email('ivan@gmial.com')
# valid: False · suggestion: 'ivan@gmail.com'

Syntax, a live mail server (MX), disposable domain, role mailbox — and, above all, a typo suggestion. ZeroBounce and Hunter charge for this; here it is DNS-over-HTTPS plus a bundled disposable-domain list, no third-party keys. With check_mx='0' it answers instantly and offline.

pwned — has this password leaked

k.pwned('qwerty123')      # pwned: True · count: 13871714

Checks Have I Been Pwned (900M+ passwords) without sending the password. The SHA-1 is computed where the key runs, and only the first five hex characters leave — you can't tell which password was asked (k-anonymity). Run it locally and the password never leaves your machine; through our server it reaches us in the clear (we don't log it) — for a real password use it locally or pass a ready SHA-1 in hash=.

password — generate or rate a password

k.password(length=20).password    # strong, via secrets, no network
k.password(check='qwerty123')     # weak · 10 bits · cracked instantly

An honest rating — entropy in bits, not «has an uppercase letter» checkboxes.

With no server of ours at all

A key is code, not a service. The library can run it on your side: the request goes to the source straight from your machine, with us out of the chain.

from monokeys import Local

k = Local()                      # no address, no access key
k.weather('Berlin').description  # 'overcast'
k.time('Haifa', to='Tokyo', at='23:00').to_time
k.read('https://example.com/article').text

Everything is the same, field names and short lines included: it is literally the same key code that runs on the server — it lives in the package, and the server takes it from there. Two copies would drift apart on the first edit.

For async code there is AsyncLocal with the same calls:

from monokeys import AsyncLocal

k = AsyncLocal()
weather = await k.weather('Berlin')

What the client itself offers:

k.names()              # which keys exist
k.describe('weather')  # the key's full manifest

A separate class rather than guesswork: a call that returns a dict in one place and a promise in another bites late and hard.

Which to take

Local Keys (our server)
speed one hop instead of two plus the round trip to us
privacy we never see your queries we do
our outages do not matter do
cache in process memory shared, survives restarts
needs outbound internet yes no, reaching us is enough
other languages Python only any, it is plain HTTP

Take Local when in doubt. Keys is for when you have no direct way out, when a cache shared across machines helps, or when the language is not Python.

Still zero dependencies: the local fetch is written on the standard library. If certifi happens to be around, the root certificate list is taken from it — on machines with a stale system list a perfectly alive site otherwise answers «certificate has expired».

Works with no network: timers

Not everything needs a request. A Telegram bot mutes someone for half an hour and has to remember them in exactly half an hour — keeping that count on someone else's server makes their uptime your correctness. So timers live on your side: a plain sqlite file, no network, no key, no limits.

from monokeys import Timers

t = Timers('bans.db')
t.set(f'unmute:{chat}:{user}', '30m', note='flood')

async for fired in t.stream():           # waits on its own, no poll loop
    chat, user = fired.id.split(':')[1:]
    await bot.restrict_chat_member(chat, user, permissions=ALL_ALLOWED)

Write the deadline however you like: 30, '30m', '2h', '1h30m', '15:00', '2026-09-09 15:00', datetime(...).

call what it does
Timers(path) storage in a sqlite file; ':memory:' for the process only
t.set(id, when, note) set one; the same id moves it instead of doubling
t.due() what has fired; delivered once
t.peek() the same, without taking
t.pending() who is still waiting and for how long
t.cancel(id) drop it early — an unmute by hand
t.stream() async for: waits on its own
t.wait(id) wait for one particular timer

Three decisions that make it behave under a bot:

  • The same id moves the timer. Extending a mute means replacing the deadline; otherwise the unmute happens twice, the first time too early.
  • A fired timer is delivered once. Two handlers of one bot would otherwise unban a person twice — the second time after a fresh ban.
  • Downtime is not lost. The bot was down for an hour, three unmutes came due in that hour — it gets all three at once instead of missing them.

Three ways to call any key

k.alive("@durov")                    # the whole answer: an object with fields
k.alive.members_count("@durov")      # a single field, in its own type
k.alive.text("@durov")               # a line for humans

Arguments

Keys(...) — the connection

Argument Default What it does
token KEYS_API_KEY, else the public key from the server access key; sent in a header only
base https://monoblock.casa/keys server address
timeout 20.0 how long to wait for an answer, seconds
retries 1 retries on a dropped connection (a server refusal is not retried)
user_agent monokeys/<version> how to introduce yourself to the server
k = Keys(timeout=5, retries=2)

k.<key>(...) — the call

Argument Default What it does
value — the main value: a link for alive, a question for answer
only "" return just this field instead of the whole answer
fmt "json" json — fields, text — a line for humans, bool — yes/no
timeout as the client override the wait for a single call
**params — the key's other parameters, by name
k.alive("@durov", only="members_count")   # 11005185
k.alive("@durov", fmt="bool")             # 'true'
k.alive("@durov", timeout=3)              # do not wait longer than three seconds

Arguments of particular keys

key argument what it does
answer sources which sources to ask; several separated by commas, empty — the key picks
answer lang source language: ru or en
crypto vs which currencies to price in, comma separated
time to the second place: what time it is there
time at an hour in the first place; empty — right now
weather when today, tomorrow, day after tomorrow, or days ahead
weather days how many forecast days to return, 1 to 7
weather units c — Celsius, f — Fahrenheit
read limit cut the text to this many characters; empty — all
k.answer('Haifa', sources='osm,wikidata')   # both answers, not one
k.answer('Haifa', sources='wiki', lang='en')
k.crypto('bitcoin', vs='usd,ils')

k.alive('@durov', only='title,members_count')   # several fields at once
# 'Pavel Durov · 11005185'

Ask a source in its own language: lang='en' with a Russian word will honestly find nothing — and stay silent instead of inventing something.

What you can ask the client

Call What comes back
k.names() names of every available key
k.fields("alive") which fields a key returns
k.alive.fields() the same, shorter
k.catalog(refresh=True) the full catalogue with descriptions, fetched anew
k.call("alive", "@durov") call a key whose name is only known at runtime

The answer

Answer is a dict that also answers like an object:

res = k.alive("@durov")
res.title == res["title"]     # the same thing
bool(res)                     # True if the key answered in the affirmative
dict(res)                     # a plain dict

Typos do not stay silent:

res.tittle
# AttributeError: no field 'tittle' in the answer; there is: username, url, is_alive, ...

k.alive.members_cout("@durov")
# AttributeError: key 'alive' has no field 'members_cout'; there is: is_alive, kind, ...

Errors

Exception When
AccessDenied the key is unknown, revoked, or was sent over plain HTTP
Unavailable the server is busy or a source did not answer — worth retrying
KeysError everything else: no such key, garbage input

All of them carry .status and .body. The first two subclass KeysError, so one except catches them all.

from monokeys import Keys, AccessDenied, Unavailable

try:
    res = k.alive("@durov")
except AccessDenied:
    print("the access key was not accepted")
except Unavailable:
    print("busy right now, will retry later")

Methods are not hard-coded

The list of keys and their fields comes from the server, so a new key is available at once, without updating the package:

k.names()          # ['alive', 'answer']
k.no_such_key      # AttributeError listing the ones that exist

Access keys

You do not need one — the public key works for everybody and is never metered. Your own is only for those who want a kill switch of their own; the service owner issues it, and it never expires either. Put it in .env:

KEYS_API_KEY=kx_...

Revoke your own key if it leaks:

curl -X POST -H "Authorization: Bearer kx_..." https://monoblock.casa/keys/token/revoke

If you would rather not install anything

The very same file can simply be downloaded — the package is built from it:

curl https://monoblock.casa/keys/sdk/python > monokeys.py

Or skip the client entirely — a key is just a link:

https://monoblock.casa/keys/alive/@durov  ->  alive · channel · Pavel Durov · …

Full documentation: https://monoblock.casa/keys/client

Source: https://github.com/monorez3/keys

Metadata

Release files for monokeys 0.9.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for monokeys 0.9.0
File Size Uploaded
monokeys-0.9.0.tar.gz 90.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for monokeys 0.9.0
File Interpreter ABI Platform
monokeys-0.9.0-py3-none-any.whl Python 3 none any Details

Total release size: 197.8 kB

Release files / monokeys-0.9.0.tar.gz

Download URL monokeys-0.9.0.tar.gz
Size 90.8 kB
Tags Source
SHA-256 checksum
How to use checksums
39bddf1edbbbabf29ce2aba999aa8bc2fc267ffa6d683792eac331f89fd0220e
BLAKE2b-256 checksum
How to use checksums
14a946161cc98f39714fdfe828c46ca41211efe4e884466317b8e8cf701e7cf5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.

Transparency log

Release files / monokeys-0.9.0-py3-none-any.whl

Download URL monokeys-0.9.0-py3-none-any.whl
Size 106.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f6bc63bad20b75badb2746e09b3e5c0a5405ece4c6e743e89cb84bdc25d03658
BLAKE2b-256 checksum
How to use checksums
1dd72882f66828882c4d27f3c41442cd44693b24af51342e9d4658a5e08c4a9d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.9.0 This release

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.8

2 release files

0.2.7

2 release files

0.2.6

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page