Do you live in Los Angeles and book tennis courts through laparks.org? You may have noticed the interface is clunky; pages are slow to load, and you need to load a separate page for each location and date. Wouldn’t it be nice to have a single view of all current availability? In comes the Tennis Court Availability API!
Table Of Contents:
Usage In Google Calendar
The API responds in a variety of formats, including ics, which is generally accepted by Calendar applications like Google Calendar, Apple Calendar, etc.
Unfortunately, Google Calendar will only update the events in the calendar about once every 12 hours. This means you won’t have data as realtime as the API is able to provide. In the future, there may be a more advanced integration that is able to sync events with less delay.
You can formulate a URL that will respond with a set of events you’d like to import to your Google Calendar as an ‘Other Calendar’. If you’d like to see all available court time at Chevoit Hills and Westwood, you could use the URL
https://tenniscalendar.balancingagent.info/v1/latest?locations=chevoit_hills,westwood
Other parameters and parameter values are available, see the full API documentation here. Once you have a URL in mind, you can import the events to your Google calendar as follows:
First, click on the ‘+’ icon beside ‘Other calendars’
Then, select ‘From URL’
Finally, paste your URL into the URL field in ‘Calendar settings’
Feel free to set notification settings, so you can be alerted when a time slot becomes available or unavailable.
Now, anytime you select the Calendar in your Google Calendar view, you will see open court availability side by side with other events on your Calendar.
How It Works
This API is powered by three processes:
recurring python script that collects data
Tennis court availability data is collected by a python script running Selenium to render Javascript and collect HTML from lacity.org SERPs like this one. Because the court availability content is not available in the initial payload, but rather only once Javascript has run, it is necessary to use a browser automation tool like Selenium, instead of simply using Python requests of PyCurl.
As of now, the process runs each court locations up to every 10 minutes, running at most one at a time. This prevents the process from being too much of a nuisance to lacity.org.
Once the data has been received and parsed, it is uploaded to a Postgresql database using psycopg2. Logs and metrics are monitored using Grafana stack.
A Postgresql Database
The database layer is handled by Postgresql, a flexible, mature, and feature rich open source SQL database. As of now, the database uses asynchronous WAL replication, were writes are handled by a primary server, and reads are handled by a replica. This means that data is backed up in the event of server failure, and also that the read and write paths do not complete for resources like CPU and RAM.
Metrics are collected via Prometheus postgres_exporter, and logs are collected via Grafana Promtail, more monitoring and alerting based on server health, available disk space, and integrity of the read and write processes.
A Flask API That Serves Data
The parsing of input parameters, generation of an SQL query, querying of the Postgresql database, and formatting of a response is handled by python Flask. All endpoints and all possible parameters are handled by a single SQL query with passed in parameters.
Special care has been paid to make the API invulnerable to SQL injection, using strategies such as character escaping and checking an allow list. Logs and metrics are monitored using Grafana stack.
Where It Could Go Next
It would be really nice to be able to reserve court time as well. There is a captcha that needs to be filled in in order to reserve , and so far, the collection script does not make any attempt to fill in that captcha.
If you have a request on how this process could be improved, please feel free to reach out to me at elliot@techenthusiast.info.
Documentation
/v1/latest
Description:
Returns the latest currency exchange rate data. This info changes throughout the day.
Example request:
curl https://tenniscalendar.balancingagent.info/v1/latest
Example response:
BEGIN:VCALENDAR
X-WR-CALNAME:Tennis Calendar
VERSION:2.0
PRODID:LA Tennis Availability Bot
BEGIN:VEVENT
CATEGORIES:TENNIS,SPORTS,TENNIS COURT AVAILABILITY
DESCRIPTION:Book this court: https://www.laparks.org/discover-activities?reserve=true&location=Cheviot%20Hills%20Pay%20Tennis
DURATION:PT1H
DTSTART:20240924T190000Z
SUMMARY:Tennis at chevoit_hills Court 2
UID:2521a99c-eaea-4f65-bc0a-cfae2804ba3a
END:VEVENT
BEGIN:VEVENT
CATEGORIES:TENNIS,SPORTS,TENNIS COURT AVAILABILITY
DESCRIPTION:Book this court: https://www.laparks.org/discover-activities?reserve=true&location=BalboaPayTennis
DURATION:PT1H
DTSTART:20240927T150000Z
SUMMARY:Tennis at balboa Court 13
UID:ffe4dcb6-12d1-43af-a1cd-4b655194aff0
END:VEVENT
...
URL Parameters:
- format
- Description: The structure of the data in the response.
- Default: ics
- Possible Values: ics, json, csv, xml
- Example:
/v1/latest?format=
json
- locations
- Description: comma separated list of locations to include in the response. ‘*’ indicates all symbols available.
- Default: ‘*’
- Possible Values: chevoit_hills, westwood, poinsetta, westchester, griffith_riverside, balboa, van_nuys_sherman_oaks, stoner, mar_vista_parking
- Example:
/v1/latest?locations=chevoit_hills,westwood
Notes:
The response of this endpoint changes throughout the day.
/v1/<date YYYY-MM-DD>/<date YYYY-MM-DD>
Description:
Tennis court availability within a specific date range.
Example request:
curl https://tenniscalendar.balancingagent.info/v1/2024-09-19/2024-09-21?format=json
Example response:
{
"errors": {},
"inputs": {
"format": "json",
"start_date": "2024-09-19",
"end_date": "2024-09-21",
"locations": [
"westwood",
"van_nuys_sherman_oaks",
"chevoit_hills",
"balboa",
"poinsetta",
"westchester",
"mar_vista_parking",
"stoner",
"griffith_riverside"
],
"start_time": 0,
"end_time": 24,
"is_available": [
true
],
"is_pro": [
false,
true
],
"is_pay": [
false,
true
],
"num_hours": 1,
"num_courts": 1,
"pretty_print": true
},
"courts": [
{
"name": "chevoit_hills",
"court_name": "Court 1",
"start_time": "2024-09-19T20:00:00.000Z",
"is_available": true,
"is_pro": false,
"court_price": "$8.00",
"last_checked": "2024-09-19T20:14:20.000",
"booking_url": "https://www.laparks.org/discover-activities?reserve=true&location=Cheviot%20Hills%20Pay%20Tennis",
"id": "93e51fa0-9e2c-44cc-b2b9-b10962aae3e8"
},
{
"name": "chevoit_hills",
"court_name": "Court 6",
"start_time": "2024-09-19T20:00:00.000Z",
"is_available": true,
"is_pro": false,
"court_price": "$8.00",
"last_checked": "2024-09-19T20:14:20.000",
"booking_url": "https://www.laparks.org/discover-activities?reserve=true&location=Cheviot%20Hills%20Pay%20Tennis",
"id": "1f5c41fa-9dc0-4675-9bb2-4a4324b6c251"
},
...
}
}
URL Parameters:
- format
- Description: The structure of the data in the response.
- Default: ics
- Possible Values: ics, json, csv, xml
- Example:
/v1/2024-09-19/2024-09-21?format=
json
- locations
- Description: comma separated list of locations to include in the response. ‘*’ indicates all symbols available.
- Default: ‘*’
- Possible Values: chevoit_hills, westwood, poinsetta, westchester, griffith_riverside, balboa, van_nuys_sherman_oaks, stoner, mar_vista_parking
- Example:
/v1/2024-09-19/2024-09-21?locations=chevoit_hills,westwood
Leave a Reply