Skip to main content

va-lis-client

Python client for the Virginia Legislative Information System (LIS) REST API.

Built from live API responses — the official OpenAPI specs are unreliable (response envelope keys, field casing, and parameter behavior all differ from the docs).

This package doubles as the API documentation, recording behavior found nowhere else, such as pagination.

Installation

pip install va-lis-client

With Redis rate limiting support:

pip install "va-lis-client[rate-limiting]"

Quick start

import os
from va_lis_client import LISClient

# Pass the key directly, or set the LIS_API_KEY environment variable
client = LISClient(api_key="your-api-key-here")
# or: os.environ["LIS_API_KEY"] = "your-key"; client = LISClient()

# The GA's current working session. During the interim this is the
# *upcoming* session, not the one that just ended — see
# "Bill identity & carry-over" below.
session = client.get_default_session()

# All bills in that session
bills = client.get_session_bills(session_code=int(session.SessionCode))

# Full detail for a bill
bill = client.get_bill(legislation_id=98525)

# Bill text (HTML) and summaries
texts = client.get_bill_text_detail(legislation_id=98525, session_code=20261)
summaries = client.get_bill_summaries(legislation_number="HB1", session_code=20261)

Service layer

LISClient is transport: one method per endpoint, and no interpretation. LISService sits above it. It does the work every caller would otherwise repeat: resolving human bill numbers to IDs, joining the reference vocabularies, caching the large lists, and flattening bill text HTML.

from va_lis_client import LISClient, LISService, strip_html
from va_lis_client.models import CHIEF_PATRON

service = LISService(LISClient())   # or LISService() to build the client for you

# Bill numbers, not surrogate keys. Case and padding are normalized.
bill = service.get_bill("hb0001", 20261)
bill_id = service.resolve_bill_id("HB1", 20261)  # ~1.3 KB, via the text list
item = service.resolve_bill("HB1", 20261)        # the session row; needs the 3 MB list

# Text: the newest version by default, or name one by DocumentCode.
item, detail = service.bill_text("HB1", 20261)
item, detail = service.bill_text("HB1", 20261, "HB1ER")
print(strip_html(detail.DraftText))

# Who voted which way, with party and district already attached.
for row in service.roll_call("HB1", 20261):
    print(row.name, row.party, row.district, row.response)
# Lashrecse D. Aird   D  13th  Y
# Luther Cifers, III  R  10th  N

# The other axis: everything one member voted on.
for v in service.member_votes(503, 20261):
    print(v.bill_number, v.response, "BLOCK" if v.is_block else "")
votes = service.member_votes_on(503, "hb0001", 20261)

# Members by name, and the bills a member patrons, co-patronage included.
schmidt = service.find_members("schmidt", 20261)[0]            # MemberID 544
for b in service.member_bills(schmidt.MemberID, 20261):
    print(b.LegislationNumber, b.LegislationStatus, b.SummaryVersion)
chief = service.member_bills(schmidt.MemberID, 20261, role=CHIEF_PATRON)
patrons = service.bill_patrons("HB1408", 20261)                # every role

# Committees: who sits where, with party and district, and the reverse.
courts = service.resolve_committee("Courts of Justice", "H")   # H08
for s in service.committee_members(courts.CommitteeID, 20261):
    print(s.name, s.role, s.party, s.district)
seats = service.member_committees(schmidt.MemberID, 20261)     # 14 cached requests

# Senate dockets: every bill a committee has scheduled, with the date and room.
for e in service.docket_entries("Courts of Justice", 20261):
    print(e.date, e.bill_number, e.room)

# Every vote including the ones no member can be credited with.
for record in service.bill_votes("HB1", 20261):
    notes = record.statements_by_member()
    for code, members in record.responses().items():   # "Y" "N" "A" "X"
        for m in members:
            print(code, m.MemberNumber, m.name, notes.get(m.MemberID, []))

# Reference joins. Events carry a null type ID and a null status ID, so these
# index the vocabularies on the keys that events actually carry.
types = service.event_types_by_code()          # EventCode -> [LegislationEventType]
kind = service.event_type_for(event)           # the one row describing an event
statuses = service.statuses_by_name()          # Name      -> LegislationStatus
label = service.bill_status_label(bill, item)  # best available status label

Voting records. bill_votes walks a bill's events, takes each VoteID, and fetches the roll call behind it. Events are the only bill-first route, because no vote endpoint accepts a legislationID. HB1 in 20261 returns eight votes: two House committee, one House floor, two Senate committee, three Senate floor. Six of the eight are attributable to HB1 alone. See Votes for why the other two are not.

roll_call is bill_votes with the roster joined on. A ballot from LIS names a member by MemberID and nothing else, so party and district take a second lookup against the session roster, cached for an hour. roll_call returns only the attributable votes, since naming a member on a block or voice vote misstates the record. HB1 gives 214 named ballots across its six roll calls. Each entry carries its own vote, because one bill has many.

EventCode is not unique. The event-type vocabulary holds 3,912 rows under only 1,502 codes, and H1405 alone returns four. Two of those four read "Reported from Labor and Commerce" and two read "Failed to report (defeated) in Labor and Commerce". event_types_by_code therefore returns every row per code, and event_type_for picks the one that describes a given event. See Joining the event vocabulary.

Resolving an id cheaply. The text list endpoint takes a bill number and returns rows carrying the LegislationID, so resolve_bill_id costs about 1.3 KB instead of the 3 MB session list. It is session scoped, so it will not resolve a bill into a session it does not belong to. Bills with no published text fall back to the list. Use resolve_bill when you want the session row itself, for its status, description, or chief patron.

Caching. The session bill list backs substring search and is the fallback for id resolution. It runs to roughly 3 MB, so LISService caches it for 15 minutes (tune with bill_list_ttl=). The member roster changes only when a member resigns or arrives, so it is cached for an hour (roster_ttl=), and a member's 1.9 MB vote history rides the same TTL, keyed per member. A member's bill list rides it too, keyed per member, session, and role. The static reference vocabularies, the session reference included, are kept for the life of the instance. Caches live on the instance, so build one service and keep it. Instances are safe to share between threads.

Errors. Everything derives from LISError, so one except clause covers the package. The resolution errors also derive from the builtin that fits them.

Exception Also a Raised when
LISClientError The API returns a non-success response
InvalidBillNumberError ValueError A string does not parse as a bill number
BillNotFoundError LookupError A bill number is not in the session
TextVersionNotFoundError LookupError No text version matches, or the version has no body
SessionNotFoundError LookupError A session code is not in the session reference
CommitteeNotFoundError LookupError No committee, or more than one, matches a number or name

Authentication

Every request sends a WebAPIKey header with a partner GUID obtained at https://lis.virginia.gov/apiregistration.

Set the key via environment variable:

export LIS_API_KEY=your-guid-here

Or pass it directly to the constructor:

client = LISClient(api_key="your-guid-here")

Important: Heartbeat endpoints accept any key — even garbage. Use client.check_api_key() to actually validate a key via the PartnerAuthentication service.

Data availability

  • The session reference list reaches back to 1994.
  • Bill data has been observed working for the 2024–2027 sessions (as of Aug 2026). Earlier guidance said only 2025–2026 was authorized via the API.
  • Votes reach back to 1994 — much further than bills. voteID=1 is a January 1994 committee vote. Do not assume a vote ID belongs to a recent session.
  • Older session data is available as legacylis.virginia.gov CSV downloads.
  • No rate limit is enforced, but usage is tracked per key. DLAS said in September 2026 that they do not rate limit, that they watch per key usage, and that they have revoked keys after sending warnings. Rate limit yourself; see Rate limiting.

Identifier cheat-sheet

Bills are uniquely identified by session + bill number, but the API uses several overlapping identifiers:

Identifier Example Notes
SessionCode 20261 Year + sequence. 20261 = 2026 Regular, 20262 = Special I
SessionID 59 Surrogate PK. Either code or ID works as query param
LegislationNumber "HB1" Must be unpaddedHB0001 returns 204
LegislationID 98525 Surrogate PK for the logical bill — reused across carry-over (see below)
DocumentCode "HB1ER" Bill number + version suffix
LegislationTextID 257719 PK for one immutable text snapshot — follows the bill across carry-over (see below)
CommitteeID 14 Surrogate PK for a committee
CommitteeNumber "H14" Chamber prefix + number, e.g. H14, S02
CalendarID 21146 PK for a House floor calendar
DocketID 21114 PK for a Senate committee docket
ScheduleID 3626 PK for a scheduled meeting
LegislationEventID 1561089 PK for a bill history event — follows the bill across carry-over (see below)
VoteID 294006 PK for one vote. Reachable only from LegislationEvent.VoteID
MemberID 217 PK for a person. Stable across every vote and session
VoteMemberID 10988439 PK for one ballot — that person's response on that one vote. New every vote
MemberNumber "H0206" Chamber prefix + number, the human-facing member id
DistrictID 98 Surrogate PK for a seat. The label is DistrictName on a member, Title on a district

MemberID and VoteMemberID are not interchangeable, and one LIS field mixes them up. See The VoteStatement.VoteMemberID trap.

Bill identity & carry-over

Virginia's even-year sessions may carry unfinished bills over into the following odd-year session — never the reverse, never twice, and never across a two-year General Assembly term. The API models this by reusing the LegislationID (confirmed against the live API, 2026-08-12):

  • A carried-over bill appears in both sessions' get_session_bills lists with the same LegislationID (e.g. HB9, ID 98631, in 20261 and 20271).
  • get_bill(legislation_id) returns top-level SessionCode/SessionID as None. The Sessions list carries one cross-ref per session the bill appears in — both the even and odd session for a carried-over bill — and is the explicit lineage record.
  • So LegislationID identifies the logical bill within a GA term: at most two session appearances (even, then odd if continued). Session-scoped identity is (SessionCode, LegislationNumber) or (SessionCode, LegislationID).
  • An odd-year bill list starts as pure carry-over — as of Aug 2026, 20271 returned 443 bills, every one sharing its ID with 20261. Continued bills must be acted on by mid-November of the even year or they die, so expect that set to shrink before the odd session convenes.

Texts and events are entity-level too. A bill's text versions and history events belong to the logical bill, not to a session appearance, so they follow it across the carry-over with their ids unchanged: get_bill_texts("HB9", 20261) and get_bill_texts("HB9", 20271) return the identical record (LegislationTextID=257905, the introduced text — verified live 2026-08-12). A text id never mutates. When the text changes, LIS appends a new version record with a new id (HB9HB9EHB9H1HB9ER); a carried bill brings its whole existing stack into the new session and appends any odd-year amendments to it. Event histories behave the same way — the odd-year view includes every even-year event under its original LegislationEventID.

Sync warning: if you mirror bills into a database, don't put a global unique constraint on any of these ids. The first odd-year sync will violate a global LegislationID constraint on the first carried-over bill, violate a global LegislationTextID constraint on its first text version, and — worst — an upsert keyed on a globally-unique LegislationEventID won't error at all: it silently re-parents the even-year rows to the odd-year bill. Scope every LIS surrogate to its owning parent: (session, LegislationID), (bill, LegislationTextID), (bill, LegislationEventID).

Default session: get_default_session() tracks the GA's working session, not the last one convened. Once a session wraps up (sine die, veto session, enactments effective July 1), the default advances to the upcoming session during the interim — by Aug 2026 it was already 20271, which is where continued bills and (from mid-November) new prefiles accumulate. For retrospective work on a just-ended session, pass its explicit session code.

Response envelope gotchas

The docs say ListItems everywhere. Reality:

Endpoint Docs say Actual key
Sessions ListItems Sessions
Legislation list ListItems Legislations
Legislation detail ListItems Legislations
Legislation statuses ListItems References
Text list ListItems LegislationTextList
Text detail ListItems TextsList
Summaries ListItems LegislationSummaries
Version refs ListItems LegislationVersionList
Partner check flat PartnerList
Calendars ListItems Calendars
Dockets ListItems Dockets
Schedules ListItems Schedules
Committees ListItems Committees
Legislation events ListItems LegislationEvents
Schedule types ListItems ScheduleTypes
Meeting rooms ListItems MeetingRooms

Pagination (undocumented)

Paging works but is documented nowhere, and it is not a query string parameter. Every parameter name is silently ignored. It runs on an X-Pagination request header carrying JSON:

curl -H "WebAPIKey: $LIS_API_KEY" \
     -H 'X-Pagination: {"PageSize":3,"SkippedRecords":6}' \
     "https://lis.virginia.gov/Legislation/api/getlegislationsessionlistasync?sessionCode=20271"
Request field Effect
PageSize Caps the number of rows returned
SkippedRecords Raw record offset. This is what moves the window
PageNumber Accepted and ignored. Always echoed back as 1

Every response carries an X-Pagination header back. CurrentPage is derived from SkippedRecords divided by PageSize:

{"PageSize":3,"PageNumber":1,"TotalPages":148,"TotalCount":443,
 "CurrentPage":3,"SkippedRecords":6,"HasPrevious":true,"HasNext":true}

Using it

page = client.get_session_bills(session_code=20271, page_size=5, skip=10)

len(page)                      # 5. PagedList subclasses list.
page[0].LegislationNumber      # "HB71"
page.pagination.TotalCount     # 443
page.pagination.HasNext        # True

# TotalCount without downloading the list: one row, about a kilobyte.
client.get_session_bill_count(session_code=20271)   # 443

PagedList is a real list, so code that iterates or indexes it needs no change. .pagination is None when the server sends no usable header.

Worth using: an unpaged row is about 820 bytes, so a full regular session runs to roughly 3 MB.

Which endpoints support it

Verified on getlegislationsessionlistasync only. It is not global middleware. The event type reference endpoint ignores the header and returns all 3,912 rows (2.19 MB) on every call, which is why the client caches that one instead.

API services

Currently implemented

Session (/Session/api/)

  • get_sessions(year) → list of sessions for a year
  • get_default_session() → the current/active session

Legislation (/Legislation/api/)

  • get_session_bills(session_code) → lightweight bill list for a session
  • get_bill(legislation_id) → full bill detail with all patrons
  • get_legislation_statuses() → all 52 status reference entries

LegislationText (/LegislationText/api/)

  • get_legislation_versions() → 13 version type references (Introduced, Enrolled, etc.)
  • get_bill_texts(legislation_number, session_code) → text version list
  • get_bill_text_detail(legislation_id, session_code) → full HTML bill text

LegislationSummary (/LegislationSummary/api/)

  • get_bill_summaries(legislation_number, session_code) → bill summaries (HTML)

LegislationEvent (/LegislationEvent/api/)

  • get_bill_events(legislation_id) → chronological action history for a bill
  • get_event_types() → 3,912 event type references
  • get_actor_types() → 5 actor types (House, Senate, Committee, etc.)

Event join keys (verified 2026-08-13): an event's Status carries the internal name from the 52-status vocabulary (a closed vocabulary, not free text), and LegislationStatusID comes back null — join statuses on the name. The event-type reference rows come back with LegislationEventTypeID null — join event types on EventCode. 120 of the 3,912 types are the committee continuance family (codes ending 40/41/42, e.g. H1940 = Continued to next session in Transportation); floor and conference continuances are covered by statuses 46–48 instead. EventDate is the true action date — a committee continuance is dated on the committee vote, not at crossover.

Joining the event vocabulary

EventCode is the join key onto the event-type reference, but a code does not identify a row. Measured 2026-09-08: 3,912 rows carry only 1,502 distinct codes, and 1,326 codes repeat.

The repeats are not harmless copies. H1405 returns four rows:

LegislationChamberCode IsPassed LegislationDescription
H true Reported from Labor and Commerce
S true Reported from Labor and Commerce
H false Failed to report (defeated) in Labor and Commerce
S false Failed to report (defeated) in Labor and Commerce

A dict[str, LegislationEventType] keyed on the code keeps whichever row arrives last, so it can report a bill as defeated when it passed.

(EventCode, LegislationChamberCode, IsPassed) is the real primary key: it yields 3,912 distinct keys for 3,912 rows, with no collisions.

Two traps in that key:

  • LegislationChamberCode is the bill's chamber, not the actor's. A Senate bill reported from a House committee carries the House actor code H1405 against a row whose chamber is S. The event's own ChamberCode mirrors the code prefix, so it names the actor and cannot serve here. Take the chamber from the event's LegislationNumber instead.
  • IsPassed separates the twins. Fixing the code and chamber still leaves the pass/fail pair.

Fixing (EventCode, IsPassed) and flipping only the chamber changes LegislationDescription for just 7 codes, five of which differ by a typo or a trailing space (Privilegse for Privileges). The two real ones are G7210 (received by House versus Senate) and H4620 (Printed as engrossed versus Printed as second chamber engrossed). CalendarDescription differs across the chamber pair far more often, in 148 codes.

LISService.event_type_for(event) applies all of this.

Vote (/Vote/api/)

  • get_vote(vote_id) → one vote with its per-member roll call
  • get_vote_types() → 3 vote types (1=Committee, 2=Subcommittee, 3=Floor)

This service is absent from the LIS developer portal service list. It is the only route to per-member votes. See Votes.

Member (/Member/api/)

  • get_members(session_code, chamber_code=None) → the session roster, with party and district
  • get_member(member_id, session_code) → one member, 204s for some sitting members
  • get_parties() → 3 parties (D, I, R)
  • get_districts() → 140 districts (100 House, 40 Senate)

MemberID is the join key from a ballot. See Members.

MemberVoteSearch (/MemberVoteSearch/api/)

  • get_member_votes(member_id, session_code) → every vote one member cast

The member-first axis, where /Vote is bill-first. See The member-first axis.

LegislationByMember (/LegislationByMember/api/)

  • get_member_legislation(member_id, session_id, patron_type_id=None) → every bill a member patrons, in any role

Takes a session_id, not a session code, because the endpoint caches session codes wrongly. See Bills by member.

LegislationPatron (/LegislationPatron/api/)

  • get_bill_patrons(legislation_id) → every patron of a bill, in every role
  • get_patron_roles() → 5 roles (1 Chief Patron, 2 Chief Co-Patron, 3 Incorporated Chief Co-Patron, 4 Co-Patron, 5 Offered)

Not wired: getlegislationpatronlistasync serves chief patron relationships only (any other patronType answers 204) and unfiltered returns every chief patron in a chamber with their bills, 1.3 MB for the 2026 House; getmemberpatrontypelistasync lists the roles one member holds, including a PatronTypeID of 0 for budget amendment requests.

Committee (/Committee/api/)

  • get_committees(chamber_code=None, include_subcommittees=False) → 14 House and 11 Senate standing committees; 67 House rows with subcommittees
  • get_committee(committee_id, session_id) → one committee, with MeetingNote and EffectiveBeginDate
  • get_committee_by_number(committee_number) → the same by number, e.g. "H14"

The list ignores the session it is sent. See Committees and seats.

MembersByCommittee (/MembersByCommittee/api/)

  • get_committee_members(committee_id, session_code) → who sits on a committee, with roles; session required
  • get_committee_roles() → 8 roles, with chamber specific IDs

CommitteeLegislationReferral (/CommitteeLegislationReferral/api/)

  • get_committee_actions() → 41 committee actions

Despite the name, this service does not list the bills referred to a committee. The action vocabulary is its only data operation.

Schedule (/Schedule/api/)

  • get_schedules(start_date, end_date, owner_id=None, schedule_type_id=None, vote_room_id=None) → meetings in a date range; always pass dates
  • get_schedule_types() → 6 types (1 Committee, 2 Chamber, 3 Conference, 4 Caucus, 5 Other, 6 Docket)
  • get_meeting_rooms(chamber_code=None) → room reference, 18 for the House

Not wired: previewvcalfileasync, a vCal export.

Calendar (/Calendar/api/)

  • get_calendars(chamber_code, session_code) → a chamber's floor calendars, 56 for the 2026 House
  • get_calendar(calendar_id) → one calendar with categories, agendas, and per-member vote rows
  • get_dockets(committee_id, session_code) → a Senate committee's dockets; a House committee returns nothing
  • get_dockets_by_committee_number(committee_number, session_code) → the same by number, e.g. "S13"
  • get_docket(docket_id) → one docket with its bills, members, staff, and linked schedule
  • get_calendar_types() → 2 types (1 Chamber, 2 Committee)
  • get_calendar_category_types(chamber_code=None) → 98 category codes, e.g. CSGEN = Senate Bill in Committee

Not wired: getcalendaractionsreferenceasync returns 4,952 rows and 2 MB, and getcategorytypesreferenceasync is unprobed. See How meetings and dockets work.

Not yet explored

These services exist in the portal but haven't been investigated, except where noted:

  • AdvancedLegislationSearch (spec read 2026-09-11: a POST keyword search, getmostfrequentlegislationsasync, introduction date lists; not wired)
  • CommunicationFileGeneration
  • Contact
  • LegislationCollections
  • LegislationCommunications
  • LegislationFileGeneration
  • LegislationSubject (spec read 2026-09-11: getsubjectreferencesasync, a subject vocabulary per session; not wired)
  • MemberVoteSearch (only getmembervotelistasync is wired)
  • MinutesBook
  • Organization
  • Person
  • Personnel

Members (the roster behind a roll call)

A ballot names a member by MemberID and a display name. Party and district live in the Member service, and MemberID joins the two directly: all 100 ballots on House vote 294006 resolve against the 2026 roster.

LISService.roll_call does that join. members_by_id(session_code) exposes the cached roster if you want to do it yourself.

The roster is session scoped, and larger than the chamber

getmembersasync requires sessionCode and returns HTTP 400 without it. The rosters genuinely differ: 148 rows for 20261, 140 for 20251, 142 for 20241, with only 123 members shared between 2026 and 2024.

Session 20261 returns 148 rows for 140 seats, 106 House against 100 and 42 Senate against 40. That is correct, not a bug. The extras are members who left or arrived mid-session, plus one member who moved from the House to the Senate and so appears under two member numbers.

Do not filter on MemberStatus. Delegate Barry Knight (MemberID 217) voted on HB1 on 3 February 2026 and his service ended on 17 February with reason "Deceased". Drop the non-active rows and that vote loses its voter.

Other traps

MemberStatusID on the roster is the member's previous status. The code itself is sound, and every getmemberbyidasync response agrees on it: 1 Active, 2 Inactive, 3 Outgoing. The roster list breaks it two ways. It omits the ID on 62 of 148 rows. And on a member who has left it keeps the status they held before leaving while refreshing the name:

Member MemberStatus list MemberStatusID by-id ServiceEndDate
Barry D. Knight Inactive 1 (Active) 2 2026-02-17
Ghazala F. Hashmi Inactive 1 (Active) 2 2026-01-17
Mark D. Sickles Outgoing 1 (Active) 3 2026-01-17
Adam P. Ebbin Outgoing 1 (Active) 3 2026-02-18

All six rows whose list ID contradicts their own name look like this. Read the name, exactly as events join their status on the name.

Which member endpoint to use

There are three, and they are incomplete in different dimensions. Neither of the two useful ones is simply the better one.

getmembersasync getmemberbyidasync getmemberlistasync
Rows all 148 1, and 9 of 148 return nothing all 148
Fields 28, nine always null 29, five null 16
Wired as get_members get_member not wired

The list is complete in rows and incomplete in fields. It returns every member every time, but nine fields come back null on all 148 rows: ChamberName, SeatNumber, VotingSequence, SessionID, SessionCode, Salutation, Seniority, StatusReason, and LastElectionDate. Note SessionCode is null even though the query is session scoped, the same shape as the carry-over trap.

By-id is complete in fields and incomplete in rows. It fills four of those nine — ChamberName, SeatNumber, VotingSequence, SessionID — and adds MemberDetailID. The other five are null there too. But it answered 204 for 9 of the 148 members on the 2026 roster, including sitting ones.

Build on the list, because the two failures are not equally bad. A missing row is unrecoverable: no name, no party, no district, and no error telling you so, which leaves that member's votes unattributable. A null field costs almost nothing, because the list still carries MemberID, name, PartyCode, DistrictName, ChamberCode, and the service dates — everything a voting record needs. What by-id adds is thin: ChamberName restates ChamberCode, SessionID is what you passed in, and SeatNumber and VotingSequence are seating-chart trivia. LISService.roll_call therefore reads the roster once and never calls by-id.

The one field where the list is wrong and by-id is right is MemberStatusID, above. So: the list is authoritative for which members exist and for every field it populates except that one; by-id is authoritative for MemberStatusID and the four extra fields, when it answers.

The 204s are not a key problem. Passing identityID 204s as well, and passing an IdentityID as memberID returns a different member, so the parameter is right. Every person holding two member records fails, which accounts for the four who moved from the House to the Senate mid-term and so appear under two member numbers. The remaining five have a single record and share no trait found so far. All nine 204 in every session tried, so it is a property of the member rather than the query.

Whitespace dirt, worse than anywhere else in the API — the client strips it for you. Padded strings appear across unrelated fields, and which fields carry the padding moves between sessions:

Field 20241 20251 20261 20271
GABEmailAddress 57 57 50 45
ListDisplayName 1 1 4 4
MemberDisplayName 0 0 3 3
RoomNumber 1 0 0 0

Chasing that with a per-field accessor loses to the next field LIS pads, so LISModel strips every string at validation. MemberNumber == "H0386" and ResponseCode == "Y" hold whatever LIS sends. Member.name, .list_name, and .email remain as shorthand that never returns None. Bill detail pads patron names too: getlegislationbyidasync returned " Charlie Schmidt" as chief patron of HB1408 in 20261. Every response model therefore inherits LISModel now, not only the roster and vote models.

getmemberlistasync is strictly worse than both. It returns the same 148 rows under ShallowMembers with only 16 fields, dropping DistrictID, DistrictName, RoomNumber, and ServiceEndDate, so it can neither back a voting record nor tell you who left. It has no advantage over getmembersasync, which accepts the same undocumented chamberCode filter, so this client does not wire it.

A district labels itself Title; a member calls the same value DistrictName. Both read "71st".

Bills by member

/LegislationByMember answers "what does this member patron", in every role. The bill-first list names only the chief patron, so this is the only route to co-patronage short of one detail call per bill. find_members turns a name into a MemberID first; it searches the cached roster, so it costs nothing after the first call for a session.

schmidt = service.find_members("schmidt", 20261)[0]
for b in service.member_bills(schmidt.MemberID, 20261):
    print(b.LegislationNumber, b.LegislationStatus, b.SummaryVersion)
chief = service.member_bills(schmidt.MemberID, 20261, role=CHIEF_PATRON)

Delegate Schmidt's 2026 list: 229 bills, 11 of them as chief patron.

Send sessionID, never sessionCode

The endpoint accepts both, and sessionCode is a trap. Every response carries a CacheKeyName, and it reveals the server's cache key:

Call Cache key
memberID=544&sessionCode=20261 {MEMBERID=544}
memberID=544&sessionID=59 {MEMBERID=544}{SESSIONID=59}

A sessionCode call is honored on a cache miss, and the answer is then stored under the member alone. Every later sessionCode call for that member, from any partner, gets the first session's rows back. Member 186 queried with sessionCode=20251 and then 20271 returned the same 225 rows of session 57 both times, while sessionID=61 returned 25 rows of session 61 (verified 2026-09-11). LISClient.get_member_legislation therefore takes a session_id, and LISService.session_id(session_code) resolves the code from the session reference. Do not omit the session either: with no parameters at all the server tries to list every bill for every member, and the request hangs past 60 seconds.

One row per summary version

A bill appears once per published summary. HB18 came back three times, as introduced, as passed chamber, and as passed, so 236 rows described 229 bills. member_bills keeps the last row per LegislationID, which carries the newest summary, and preserves the order of first appearance. LISClient.get_member_legislation returns the rows as sent.

The nested patron is not the patron list

Patrons on these rows is empty on 177 of 236 and holds one entry on the rest: the chief patron, in a thin shape with no LegislationID, ChamberCode, MemberNumber, or Name, and a PatronDisplayName of "Cole, J.G." rather than "Cole". The member being listed never appears in it. Those four fields are therefore optional on Patron, and Patron.role names the role on every shape. For the real list call bill_patrons("HB1408", 20261), which returns 12 rows for that bill.

Committees and seats

/Committee lists the committees, and /MembersByCommittee says who sits on one in a session. Neither is member-first, so member_committees walks a chamber's committees and reads every seat list, all cached for an hour.

courts = service.resolve_committee("Courts of Justice", "H")
for s in service.committee_members(courts.CommitteeID, 20261):
    print(s.name, s.role, s.party, s.district)
# Patrick A. Hope   Chair   D   1st
# ...
for s in service.member_committees(544, 20261, include_subcommittees=True):
    print(s.committee.name, s.role)
# Courts of Justice                           Member
# Communications, Technology and Innovation   Member
# HCJ Sub: Criminal                           Member

The committee list is not session scoped

getcommitteelistasync accepts a session and ignores it: the cache key reads {SESSIONID=0} for every call, and 20251 and 20261 return the same 14 House committees. The seat list is the session scoped half, and it requires a session; omitting it returns HTTP 400. Courts of Justice seats 23 in 20261 and 22 in 20251.

Role IDs differ by chamber

getcommitteerolesasync returns eight rows, and the same title carries a different ID in each chamber: a House chair is 3, a Senate chair is 1, a House member is 6, a Senate member is 5. Compare CommitteeRoleTitle, not the ID. The titles are Chair, Co-Chair (Senate), Vice-Chair (House), Member, and Ex-Officio; a full committee's chair sits on its subcommittees as Ex-Officio.

Two seat shapes, and padded subcommittee names

The seat list names the role in CommitteeRoleTitle and carries no party. The seat nested in docket detail names it in Title and does carry PartyCode. CommitteeMember.role reads either. committee_members joins the session roster for party and district instead of trusting either row.

A subcommittee number is its parent's number plus a three-digit sequence (H08001), and subcommittee names arrive padded inside: "HAPP Sub: Commerce Agriculture & Natural Resources". The model strips only the ends, so Committee.name collapses the middle.

There is no "bills in committee" endpoint

CommitteeLegislationReferral sounds like one, but its only data operation is the 41-row action vocabulary. The bill list says "In Committee" without naming the committee, and bill detail nulls CommitteeName. The events carry it: read LegislationEvent.CommitteeName on a bill's history for the committee it sits in.

The member-first axis

/Vote answers "who voted on this bill". /MemberVoteSearch answers the other direction: "how did this member vote on everything". The two agree — member_votes_on(503, "HB1", 20261) and roll_call("HB1", 20261) return the same VoteID values with the same responses, and a live test asserts it.

for v in service.member_votes(503, 20261):
    print(v.bill_number, v.response, "BLOCK" if v.is_block else "")

Delegate Anthony's 2026 record: 2,867 bill positions over 2,250 distinct votes, 2,179 floor, 437 committee, 251 subcommittee.

A row is a (vote, bill) pair, not a vote

A block vote repeats, once per bill it disposed of. One House vote in that record — VoteID 297750, "Read third time and passed House (97-Y 0-N 0-A)" — appears 105 times, once for each bill it passed. So len(rows) counts bill positions and overstates how often a member voted. Count distinct VoteID for votes cast.

This endpoint carries no IsBlock flag, unlike a Vote record. The service recovers it by counting the rows that share a VoteID, and the count is exact: for vote 297750 it derives 105, and Vote.vote_legislation also holds 105. Read MemberVote.is_block before treating a row as the member's verdict on that one bill.

ClassificationName is not the legislation filter

It takes three values, and only one of them means "not a bill":

ClassificationName Rows Has a bill? What it is
Legislation 2,179 yes floor votes on bills
null 688 yes committee and subcommittee votes on bills
Attendance 43 no quorum roll calls

Filter on LegislationNumber. Filtering on ClassificationName == "Legislation" silently drops every committee and subcommittee vote the member cast — 688 of them here, nearly a quarter of the record. member_votes filters correctly and takes legislation_only=False if you want the attendance rows too.

Other notes

It is the heaviest response in the API, roughly 1.9 MB per member, and you cannot ask for several members at once. Both memberID and sessionCode are required, and omitting either returns HTTP 400. chamberCode is accepted and silently ignored — it returns a byte-identical response. LISService caches the payload per member and session for roster_ttl.

VoteStatement the field collides with VoteStatement the model, so the scalar is aliased to vote_statement on the Python side. It is always null in practice; the real corrections are in VoteStatements, whose VoteMemberID holds a MemberID like everywhere else.

Votes (per-member roll calls)

The /Vote service is the only route to who voted how. It covers both chambers, and committee, subcommittee, and floor votes alike.

There is no bill-first endpoint. Every getvotesby...legislation... name returns 404. Reach a vote through an event:

Legislation ──→ LegislationEvent.VoteID ──→ /Vote/api/getvotebyidasync
                                              └── VoteMember[]  (the roll call)
                                              └── VoteLegislation[] (bills covered)
                                              └── VoteStatements[] (corrections)

LISService.bill_votes(bill_number, session_code) does that walk. It costs one request per vote.

A VoteID on a bill event is often not that bill's roll call

Three flags decide whether you may attribute the members to the bill:

Flag Meaning Example
IsVoice No members recorded at all. VoteMember is empty and VoteTally reads (Voice Vote) vote 300174
IsBlock One roll call disposing of many bills at once. Read vote_legislation for the count vote 300173: 40 members, 50 bills
IsPublic False on some block and voice votes vote 300174

A voice vote is not always a block: SB1 has a single-bill voice vote. Check both flags, not one.

BillVote.is_roll_call applies this test, and bill_votes(..., roll_calls_only=True) filters on it. HB1 in 20261 has eight votes; six survive the filter.

What a block vote actually is

A chamber does not vote on every bill separately. It bundles the uncontroversial ones and disposes of them in a single motion. Members vote once, and that one vote passes the whole bundle.

Vote 297750 is the extreme case in the 2026 session:

Date         2026-02-17
Chamber      H, Floor
Description  Read third time and passed House (97-Y 0-N 0-A)
IsBlock      True
members       99 recorded  (97 Y, 2 X)
bills        105 disposed

97 delegates said yes and nobody said no. That lopsidedness is the signature. Anything contested gets pulled out and voted on by itself, so blocks and single-bill votes sit side by side on the same day:

voteID   block  bills  tally
294005    True     27  (98-Y  0-N 0-A)    the bundle
294007   False      1  (93-Y  5-N 0-A)    pulled out
294021   False      1  (63-Y 35-N 0-A)    pulled out, genuinely fought
294054    True     19  (39-Y  0-N 0-A)    a Senate bundle

Every bill still gets its own event off the shared vote. LIS writes a separate LegislationEventID per bill, all pointing at the one VoteID. So a bill's history looks like an ordinary passage, and the VoteID on it is shared with 104 other bills.

Do not try to spot this in the description text. Only 9 of vote 297750's 105 bill events say "Block Vote"; the other 96 read like an ordinary individual passage:

  96x  'Read third time and passed House (97-Y 0-N 0-A)'
   9x  'Read third time and passed House  Block Vote (97-Y 0-N 0-A)'

Vote 294005 labels 1 of 27, and vote 294054 labels 2 of 19. Test IsBlock.

Why this matters for reporting. "Delegate Anthony voted Yes on HB150" is true and misleading. She voted yes to a bundle of 105 bills that nobody in the chamber opposed, which is not a considered position on HB150. That is why roll_call excludes block votes and MemberVote.is_block exists.

Who voted which way

BillVote.responses() groups the members by ResponseCode:

Code Meaning
Y yea
N nay
A abstain
X not voting

X is confirmed by a vote statement that reads "Delegate Knight was recorded as not voting" against Knight's X row on vote 294006.

Each member carries MemberID, MemberNumber (e.g. H0206), MemberDisplayName, and PatronDisplayName (the surname alone). Use VoteMember.name rather than MemberDisplayName: LIS ships leading-space dirt on some rows, 2 of the 100 on vote 294006.

Party and district are not in the roll call. They live in the Member service, which is probed but not yet wired.

HB1  House floor  2026-02-03  (64-Y 34-N 0-A)

  yea (64)
     H0386  Jessica L. Anderson
     H0353  Bonita G. Anthony
     H0253  Terry L. Austin      <-- recorded as yea. Intended to vote nay.
     ...
  nay (34)
     H0333  Jason S. Ballard
     ...
  not voting (2)
     H0370  Karen Keys-Gamarra
     H0206  Barry D. Knight      <-- recorded as not voting. Intended to vote nay.

Other traps

The tally string omits X. House floor vote 294006 returns 100 member rows — 64 Y, 34 N, 2 X — against a tally of (64-Y 34-N 0-A). Summing the members will not reproduce the tally, and the X members are neither yes, no, nor the abstentions the tally counts.

VoteStatements carries corrections the roll call does not reflect. Vote 294006 holds two, including "Delegate Austin was recorded as yea. Intended to vote nay." The member rows are not amended. Both facts are true, and only the recorded one counts. Surface the statements alongside any voting record you publish.

The VoteStatement.VoteMemberID trap

VoteStatement.VoteMemberID does not hold a VoteMemberID. It holds a MemberID. The obvious join matches nothing, returns an empty result, and raises no error.

Every row in a vote's VoteMember list carries two different IDs:

Column Identifies Stable across votes?
MemberID The person Yes. Barry Knight is 217 everywhere
VoteMemberID The ballot — one person's response on one vote No. New every vote

The same delegate on two different votes:

bill  voteID   MemberID  VoteMemberID  response
HB1   294006   217       10988439      X
HB5   297524   217       11092143      X

Now the two statement rows on vote 294006:

VoteStatementID  VoteMemberID  VoteStatement
126              17            Delegate Austin was recorded as yea. Intended to vote nay.
224              217           Delegate Knight was recorded as not voting. Intended to vote nay.

And the member rows they concern:

MemberID  VoteMemberID  ResponseCode  name
17        10988428      Y             Terry L. Austin
217       10988439      X             Barry D. Knight

The statement says 217. That is Knight's MemberID, not his VoteMemberID of 10988439.

statement.VoteMemberID == member.VoteMemberID   # WRONG — never matches
statement.VoteMemberID == member.MemberID       # right

The ranges do not overlap at all, so the wrong join fails silently for every statement on every vote: statement IDs on this vote are 17 and 217, while the ballot IDs run from 10988428 to 10988537.

BillVote.statements_by_member() applies the correct join and keys the result by MemberID. Without it, the only link from a correction to a member is the surname inside the English sentence.

The vote's EventCode can disagree with the event's. Vote 294006 reports H9999; the event pointing at it reports H5000. Join on VoteID alone.

Smaller ones. VotingSequence appears on committee votes and is absent on floor votes. VoteFile.TextFormatID is a string ("5") here, where CalendarFile.TextFormatID is an int. voteID=0 returns HTTP 400 "Failed, Database Error (51000)" rather than an empty result; an out-of-range ID returns a clean 204.

How meetings and dockets work

Committee ──→ Docket (Senate) or Calendar (House) ──→ Schedule ──→ Room
              │                                        │
              │ has DocketItems/Agendas               │ has ScheduleDate,
              │ (which bills are up)                   │ ScheduleTime (free-text!),
              │                                        │ RoomDescription,
              └────────────────────────────────────────│ IsCancelled
                                                       └──→ VoteRoom reference
  • Schedule is the "when and where", but ScheduleTime is often free text like "15 minutes after adjournment"
  • Docket/Calendar is the "what": which bills are on the agenda
  • Committee is the "who": membership, chair, staff
# Senate: every bill on every docket of a committee, one request per docket.
for e in service.docket_entries("Courts of Justice", 20261):
    print(e.date, e.bill_number, e.room)

# Both chambers: a week of committee meetings, cancelled ones included.
for m in client.get_schedules("2026-02-02", "2026-02-06", schedule_type_id=1):
    print(m.ScheduleDate, m.ScheduleTime, m.OwnerName, m.IsCancelled)

# House: a floor calendar's bills, and the votes taken on each.
calendar = client.get_calendar(client.get_calendars("H", 20261)[0].CalendarID)
for agenda in calendar.bills:
    print(agenda.LegislationNumber, [i.VoteID for i in agenda.AgendaItems])

Always give the schedule a date range. Unfiltered, getschedulelistasync returns every meeting it holds: 3,631 rows and 2 MB from October 2022 to December 2026. The week of 2026-02-02 is 147 rows, 8 of them cancelled and 35 with a blank ScheduleTime. Rows that are not committee meetings (caucuses, press events) omit OwnerID and CommitteeNumber from the JSON entirely.

Dockets are Senate only, and a docket and its schedule can disagree on the hour. A House committee answers 204 on the docket list, which the client returns as an empty list. Senate Courts of Justice has 16 dockets in 20261. Docket 21123 carries DocketDate 2026-03-09T16:30:00 while its linked schedule reads 8:00 AM for the same day, and the API does not say which is right. The docket detail envelope also reports Success: false with a null message on a complete response; the client ignores the flag.

House committee agendas are not in the API. The calendar list returns floor calendars only (all 56 of the 2026 House list are type Chamber), and no docket exists for a House committee, so "when will House committee X hear bill Y" has no endpoint as probed on 2026-09-11. The schedule gives the meeting, the bill's events give the referral, and the agenda itself lives on the committee's web page.

The calendarDate filter answers 204. get_calendars fetches the whole list, which is small, and leaves the date filter to you.

How a bill becomes law (Virginia)

A bill's lifecycle through the General Assembly, mapped to LIS status IDs. Virginia has a bicameral legislature (House of Delegates + Senate). A bill must pass both chambers in identical form before going to the Governor.

The happy path

                      ORIGINATING CHAMBER
                      ───────────────────
┌─ Prefiled (Nov–Jan) ──→  Introduced (1)
│                              │
│                              ▼
│                        In Committee (2)
│                         ┌────┴────┐
│                         │         │
│                    In Subcommittee  Left In Committee (20)
│                       (19)         = dead, session ends
│                         │
│                         ▼
│                     Reported Out (3 or 52)
│                     Committee votes to send
│                     bill to the full chamber
│                              │
│                              ▼
│                      Floor vote in originating chamber
│                      Passed House (4) or Passed Senate (5)
│
│                       CROSSING OVER
│                       ─────────────
│                     Bill goes to the other chamber
│                     and repeats: Committee → Floor
│                              │
│                              ▼
│                      Passed Both Chambers (6, 39, 40)
│                      = "Enrolled" version created (ER)
│                              │
│                              ▼
│
│                        GOVERNOR
│                        ────────
│                     Communicated (13) → With Governor (7)
│                              │
│                              ▼
│                     Awaiting Governor's Action (38)
│                         ┌────┼────────────┐
│                         │    │             │
│                         ▼    ▼             ▼
│                    Approved  Gov's      Gov's Veto
│                      (8)   Recommend.   (24, 26)
│                       │    (23, 27)        │
│                       ▼       │            ▼
│                    Enacted    ▼         Override vote
│                     (36)   Chambers      or bill dies
│                       │    adopt? (33)
│                       ▼       │
│                    Acts of    ▼
│                    Assembly  Back to
│                    Chapter   Enrolled
│                     (25)
└────────────────────────────────────────────────

Key status groups

Filing & introduction:

ID DisplayName Internal Name What happened
11 Preview Preview Prefiled but session hasn't started yet
1 Introduced Introduced Formally introduced on the chamber floor

Committee phase — where most bills die:

ID DisplayName Internal Name What happened
2 In Committee In Committee Referred to a standing committee
37 Committee Referral Pending Committee Referral Pending Awaiting committee assignment
19 In Subcommittee In Subcommittee Referred to a subcommittee
20 Left In Committee Left In Committee Bill was never voted on — effectively killed
41 In Committee Continued From Carried over from a previous session into committee
3 In House Reported Out-House Committee voted to advance (House side)
52 In Senate Reported Out-Senate Committee voted to advance (Senate side)
30 Incorporated Incorporated Merged into another bill

Floor votes:

ID DisplayName Internal Name What happened
4 Passed House Passed House Full House approved
5 Passed Senate Passed Senate Full Senate approved
12 Engrossed Engrossed Passed one chamber, snapshot before crossover
28 Engrossed Engrossed with Amendment Same but with amendments applied
29 Engrossed Reengrossed with Amendment Re-amended after engrossment

Conference — when the two chambers passed different versions:

ID DisplayName Internal Name What happened
34 Conference Requested Conference Requested One chamber asks for a conference committee
16 In Conference In Conference Conference committee is negotiating
31 In Conference Conference Report Agreed Conferees reached agreement
32 In Conference Conference Report Rejected Conferees' report was rejected
39 Passed Conference Report Adopted Both chambers accepted the conference version
45 Failed Failed in Conference Conferees couldn't agree — bill dies

Passed both chambers:

ID DisplayName Internal Name What happened
6 Passed Passed Both Both chambers approved identical text (→ version 3, Enrolled)
40 Passed Passed Generic "passed" status
35 Enrolled Enrolled-House Final enrolled text prepared (House origin)
44 Enrolled Enrolled-Senate Final enrolled text prepared (Senate origin)

Governor pipeline:

ID DisplayName Internal Name What happened
13 Communicated Communicated Bill formally transmitted to the Governor
7 With Governor With Governor Governor has received the bill
14 Awaiting Signature Awaiting Signature Waiting for physical signature
38 Awaiting Governor's Action Awaiting Governor's Action Governor is reviewing
8 Approved Approved Governor signed it (→ version 4, Chaptered)
36 Enacted Enacted Becomes law (→ version 4, Chaptered)
25 Acts of Assembly Chapter Acts of Assembly Chapter Assigned a chapter number
23 Governor's Recommendation Governor's Recommendation Sent back with proposed amendments (→ version 10)
27 Governor's Recommendation Governor's Recommendation Same (no version link)
33 Governor's Recommendation Adopted Gov Recommendation Adopted Chambers accepted the amendments
24 Governor's Veto Governor's Veto Governor vetoed (→ version 9, Veto Explanation)
26 Governor's Veto Governor's Veto Same (no version link)

Terminal / carry-over:

ID DisplayName Internal Name What happened
9 Failed Failed Bill defeated on a floor vote
10 Continued Continued To Carried over to next session
46 Continued Continued to House Carried over, sitting in House
47 Continued Continued to Senate Carried over, sitting in Senate
48 Continued Continued to Conference Carried over, in conference

Why there are 52 statuses for ~15 states

LIS uses separate status IDs to track how a bill entered a logical state. For example, "In Conference" has four IDs distinguishing whether the conference just started, the report was agreed, rejected, or continued. The Name (internal) captures the sub-reason; the DisplayName (user-facing) collapses them.

Version-linked statuses

Some statuses have a LegislationVersionID indicating which bill text version corresponds to that state:

Status Version ID Version Name
Passed Both (6) 3 Enrolled
Approved (8) 4 Chaptered
Enacted (36) 4 Chaptered
Engrossed with Amendment (28) 2 Engrossed
Reengrossed with Amendment (29) 6 Reengrossed
Governor's Recommendation (23) 10 Gov Recommendation
Governor's Veto (24) 9 Veto Explanation
Reenrolled-House (42) 7 Reenrolled
Reenrolled-Senate (43) 7 Reenrolled

Rate limiting (optional)

LIS enforces no rate limit, but DLAS tracks usage per key and has revoked keys after sending warnings (stated September 2026). No threshold is published. Treat self imposed rate limiting as required in production, even though the extra is optional to install.

If you're running many workers and want to share a global rate limit across them, install the rate-limiting extra and use try_acquire_request_permit:

pip install "va-lis-client[rate-limiting]"
import redis
from va_lis_client.rate_limiter import try_acquire_request_permit

r = redis.from_url("redis://localhost:6379/0")

if try_acquire_request_permit(redis_client=r, rate_limit=100):
    client.get_session_bills(session_code=20261)
else:
    # back off
    ...

Or set REDIS_URL and LIS_RATE_LIMIT environment variables and call with no arguments — the module picks them up automatically.

Windows / government CA roots

On Windows, Python's OpenSSL doesn't automatically load system certificates, which can cause SSL errors when connecting to hosts with enterprise or government CA roots. va-lis-client includes a SystemCertSSLAdapter that loads the system certificate store automatically, so no manual cert bundling is needed.

Package structure

src/va_lis_client/
├── __init__.py         # re-exports the client, the service, and the errors
├── client.py           # HTTP client — returns Pydantic models
├── service.py          # LISService — resolution, reference joins, text helpers
├── exceptions.py       # LISError and its subclasses
├── http.py             # SystemCertSSLAdapter + shared requests session
├── rate_limiter.py     # optional Redis-based rate limiter
└── models/
    ├── __init__.py     # re-exports all models
    ├── common.py       # Heartbeat, Partner
    ├── session.py      # Session, SessionEvent
    ├── legislation.py  # Patron, Legislation, LegislationStatus, ...
    ├── text.py         # LegislationTextItem, LegislationTextDetail, ...
    ├── event.py        # LegislationEvent, LegislationEventType, ActorType
    ├── pagination.py   # Pagination, PagedList — the X-Pagination protocol
    ├── committee.py    # Committee, CommitteeMember, CommitteeAction
    ├── schedule.py     # Schedule, ScheduleType, MeetingRoom
    ├── calendar.py     # CalendarDetail, Agenda, ...
    ├── vote.py         # Vote, VoteMember, VoteLegislation, VoteStatement
    ├── member.py       # Member, Party, District
    ├── member_vote.py  # MemberVoteResult — the member-first axis
    └── docket.py       # DocketDetail, DocketItem, DocketCategory, ...

License

MIT

Release files for va-lis-client 0.3.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 va-lis-client 0.3.0
File Size Uploaded
va_lis_client-0.3.0.tar.gz 142.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for va-lis-client 0.3.0
File Interpreter ABI Platform
va_lis_client-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size:230.4 kB

Release files / va_lis_client-0.3.0.tar.gz

Download URL va_lis_client-0.3.0.tar.gz
Size 142.1 kB
Tags Source
SHA-256 checksum
How to use checksums
3e2599d40df0f0c81a0bf6ff9212a3310b056bd61a2ab242bfe849c638d44de3
BLAKE2b-256 checksum
How to use checksums
5a4edff9afcd58affc76cb6640bf19c20e2948adf27d86b0e842b472a89b5ec9
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 11, 2026.

Transparency log

Release files / va_lis_client-0.3.0-py3-none-any.whl

Download URL va_lis_client-0.3.0-py3-none-any.whl
Size 88.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
be765d0d88a007a5d1f714d7fac0e3af426ac5a29e207da544b8483e2afd3e61
BLAKE2b-256 checksum
How to use checksums
5e40084c0036decf9bb05c503756524981852462d2adb9ab136162b8e1fa5c46
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 11, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.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