mirror of
https://github.com/nikdoof/foursquare-feeds.git
synced 2025-12-13 08:52:23 +00:00
Compare commits
5 Commits
f1496a2194
...
d03fcc0b3d
| Author | SHA1 | Date | |
|---|---|---|---|
|
d03fcc0b3d
|
|||
|
f7bf00ab1c
|
|||
|
dd11c3c7b4
|
|||
|
d82817cad3
|
|||
|
a5c1c9a6d0
|
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
6
.github/renovate.json
vendored
Normal file
6
.github/renovate.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base",
|
||||
"github>nikdoof/renovate-config:python"
|
||||
]
|
||||
}
|
||||
33
.github/workflows/build-container.yaml
vendored
Normal file
33
.github/workflows/build-container.yaml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Build Container
|
||||
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository }}:${{ github.ref_name }}
|
||||
ghcr.io/${{ github.repository }}:latest
|
||||
30
.github/workflows/lint.yaml
vendored
Normal file
30
.github/workflows/lint.yaml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Lint
|
||||
|
||||
'on':
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.12", "3.13"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
enable-cache: true
|
||||
- name: Install dependencies
|
||||
run: uv sync
|
||||
- name: Lint with ruff
|
||||
run: uv run ruff check
|
||||
33
.github/workflows/release.yaml
vendored
Normal file
33
.github/workflows/release.yaml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Build Release
|
||||
run: uv build
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: "Version ${{ github.ref_name }}"
|
||||
files: |
|
||||
dist/*
|
||||
209
.gitignore
vendored
209
.gitignore
vendored
@@ -1,4 +1,207 @@
|
||||
config.ini
|
||||
.vscode
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[codz]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
#poetry.toml
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||
#pdm.lock
|
||||
#pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# pixi
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||
#pixi.lock
|
||||
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||
.pixi
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.envrc
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Abstra
|
||||
# Abstra is an AI-powered process automation framework.
|
||||
# Ignore directories containing user credentials, local state, and settings.
|
||||
# Learn more at https://abstra.io/docs
|
||||
.abstra/
|
||||
|
||||
# Visual Studio Code
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the entire vscode folder
|
||||
# .vscode/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Marimo
|
||||
marimo/_static/
|
||||
marimo/_lsp/
|
||||
__marimo__/
|
||||
|
||||
# Streamlit
|
||||
.streamlit/secrets.toml
|
||||
|
||||
# App specific files
|
||||
*.ics
|
||||
*.kml
|
||||
config.ini
|
||||
@@ -1,10 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## 2.3.1 - 2025-08-??
|
||||
## 2.4.0 - 2025-08-??
|
||||
|
||||
- Add support for pushing events to a CalDAV server.
|
||||
- Add a more detailed description to the events
|
||||
-
|
||||
- Add a more detailed description to the events.
|
||||
- Remove KML support.
|
||||
|
||||
## 2.3.0 - 2024-11-30
|
||||
|
||||
|
||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
# Build stage
|
||||
FROM python:3.12-slim AS builder
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
|
||||
WORKDIR /app
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN uv sync --frozen --no-dev
|
||||
|
||||
# Result
|
||||
FROM python:3.12-slim
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/.venv /app/.venv
|
||||
COPY generate_feeds.py ./
|
||||
RUN mkdir -p /app/config && \
|
||||
useradd --create-home --shell /bin/bash app && \
|
||||
chown -R app:app /app
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
USER app
|
||||
|
||||
# Set entrypoint with config file option
|
||||
ENTRYPOINT ["python", "generate_feeds.py", "-c", "/app/config/config.ini"]
|
||||
CMD ["--help"]
|
||||
317
README.md
317
README.md
@@ -1,177 +1,264 @@
|
||||
# Foursquare Feeds
|
||||
|
||||
A Python script that will generate iCal (`.ics`) or KML files of your checkins on [Foursquare][4sq]/[Swarm][swarm].
|
||||
A Python tool that downloads your check-ins from [Foursquare][4sq]/[Swarm][swarm] and converts them into calendar events. You can either generate an iCal (`.ics`) file or sync directly to a CalDAV server.
|
||||
|
||||
If you set it up to save the iCal file to a publicly-visible location on a webserver, and run the script regularly, you can subscribe to the feed in your favourite calendar application.
|
||||
|
||||
A KML file can be loaded into a mapping application (such as Google Earth or
|
||||
Maps) to view the checkins on a map.
|
||||
|
||||
Foursquare [used to have such feeds][feeds] but they've stopped working for me.
|
||||
[I wrote a bit about this.][blog]
|
||||
Perfect for keeping a record of your travels and activities in your preferred calendar application.
|
||||
|
||||
[4sq]: https://foursquare.com
|
||||
[swarm]: https://www.swarmapp.com
|
||||
[feeds]: https://foursquare.com/feeds/
|
||||
[blog]: https://www.gyford.com/phil/writing/2019/05/13/foursquare-swarm-ical-feed/
|
||||
|
||||
## Features
|
||||
|
||||
- **iCal Export**: Generate `.ics` files that can be imported into any calendar application
|
||||
- **CalDAV Sync**: Upload check-ins directly to CalDAV servers (Google Calendar, iCloud, etc.)
|
||||
- **Flexible Fetching**: Get recent check-ins or your entire history
|
||||
- **Rich Event Data**: Includes location details, notes (shouts), and check-in metadata
|
||||
|
||||
## Installation
|
||||
|
||||
This should work with python 3.12 (and maybe others).
|
||||
This project requires Python 3.12+ and uses [uv](https://github.com/astral-sh/uv) for dependency management.
|
||||
|
||||
### 1. Make a Foursquare app
|
||||
### 1. Install uv
|
||||
|
||||
Go to https://foursquare.com/developers/apps and create a new App.
|
||||
If you don't have uv installed:
|
||||
|
||||
```bash
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
```
|
||||
|
||||
### 2. Install python requirements
|
||||
### 2. Set up the project
|
||||
|
||||
Either using [uv](https://github.com/astral-sh/uv):
|
||||
Clone the repository and create a virtual environment:
|
||||
|
||||
$ uv sync
|
||||
```bash
|
||||
git clone https://github.com/nikdoof/foursquare-feeds.git
|
||||
cd foursquare-feeds
|
||||
uv sync
|
||||
```
|
||||
|
||||
or [pip](https://pip.pypa.io/en/stable/):
|
||||
### 3. Create a Foursquare app
|
||||
|
||||
$ pip install -r requirements.txt
|
||||
1. Go to https://foursquare.com/developers/apps
|
||||
2. Create a new App
|
||||
3. Note your Client ID and Client Secret
|
||||
|
||||
### 4. Configure the application
|
||||
|
||||
### 3. Set up config file
|
||||
Copy the example configuration file:
|
||||
|
||||
Copy `config_example.ini` to `config.ini`.
|
||||
```bash
|
||||
cp config_example.ini config.ini
|
||||
```
|
||||
|
||||
Change the `IcsFilepath` and `KmlFilepath` to wherever you want your files to be saved.
|
||||
Edit `config.ini` with your settings:
|
||||
|
||||
To get the `AccessToken` for your Foursquare app, you will have to go through the sometimes laborious procedure in step 4...
|
||||
- **AccessToken**: Your Foursquare access token (see below)
|
||||
- **IcsFilepath**: Where to save the `.ics` file (for local export)
|
||||
- **CalDAV settings**: Your CalDAV server details (for direct sync)
|
||||
|
||||
## Getting an Access Token
|
||||
|
||||
### 4. Get an access token
|
||||
You need a Foursquare access token to use this tool. Here are two methods:
|
||||
|
||||
There are two ways to do this: (A) The quick way, using a third-party website or (B) the slow way, on the command line. Use (A) unless the website isn't working.
|
||||
### Method A: Quick Web-based Authentication
|
||||
|
||||
#### (A) The quick way
|
||||
1. Visit https://your-foursquare-oauth-token.glitch.me
|
||||
2. Follow the Foursquare login link
|
||||
3. Accept the permissions
|
||||
4. Copy the access token into your `config.ini`
|
||||
|
||||
Go to https://your-foursquare-oauth-token.glitch.me and follow the link to log
|
||||
in with Foursquare.
|
||||
*Thanks to [Simon Willison](https://github.com/dogsheep/swarm-to-sqlite/issues/4) for this tool.*
|
||||
|
||||
Accept the permissions, and then copy the long code, which is your Access
|
||||
Token, into your `config.ini`.
|
||||
### Method B: Manual OAuth Flow
|
||||
|
||||
That's it. [Thanks to Simon Willison for that.](https://github.com/dogsheep/swarm-to-sqlite/issues/4)
|
||||
|
||||
#### (B) The slow way
|
||||
|
||||
On https://foursquare.com/developers/apps, in your app, set the Redirect URI to `http://localhost:8000/`
|
||||
|
||||
In a terminal window, open a python shell:
|
||||
|
||||
$ python
|
||||
|
||||
and, using your app's Client ID and Client Secret in place of `YOUR_CLIENT_ID` and `YOUR_CLIENT_SECRET` enter this:
|
||||
1. Set your app's Redirect URI to `http://localhost:8000/` in the Foursquare developer console
|
||||
2. Run the following Python commands:
|
||||
|
||||
```python
|
||||
import foursquare
|
||||
client = foursquare.Foursquare(client_id='YOUR_CLIENT_ID' client_secret='YOUR_CLIENT_SECRET', redirect_uri='http://localhost:8000')
|
||||
client.oauth.auth_url()
|
||||
client = foursquare.Foursquare(
|
||||
client_id='YOUR_CLIENT_ID',
|
||||
client_secret='YOUR_CLIENT_SECRET',
|
||||
redirect_uri='http://localhost:8000'
|
||||
)
|
||||
print(client.oauth.auth_url())
|
||||
```
|
||||
|
||||
This will output something like:
|
||||
|
||||
'https://foursquare.com/oauth2/authenticate?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2F'
|
||||
|
||||
Copy the URL from your terminal *without the surrounding quotes* and paste it into a web browser.
|
||||
|
||||
Your browser should redirect to a URL like the one below, with an error about not being able to connect to the server (unless you have a webserver running locally on your machine):
|
||||
|
||||
http://localhost:8000/?code=XX_CODE_RETURNED_IN_REDIRECT_XX#_=_
|
||||
|
||||
Copy the code represented by `XX_CODE_RETURNED_IN_REDIRECT_XX` (note that there may be an extra `#_=_` on the end which *you should not copy*).
|
||||
|
||||
Back in your python shell, with that code, enter this, replacing
|
||||
`XX_CODE_RETURNED_IN_REDIRECT_XX` with the code you just copied:
|
||||
3. Visit the printed URL in your browser
|
||||
4. Copy the code from the redirect URL
|
||||
5. Get your token:
|
||||
|
||||
```python
|
||||
client.oauth.get_token('XX_CODE_RETURNED_IN_REDIRECT_XX')
|
||||
client.oauth.get_token('YOUR_CODE_HERE')
|
||||
```
|
||||
|
||||
This will output another long code, which is your Access Token.
|
||||
## Usage
|
||||
|
||||
Enter this in your `config.ini`.
|
||||
### Generate an iCal file
|
||||
|
||||
|
||||
## Run the script
|
||||
|
||||
Generate a `.ics` file:
|
||||
|
||||
$ ./generate_feeds.py
|
||||
|
||||
This should create an `.ics` file containing up to 250 of your most recent
|
||||
checkins (see `--all` argument below to get more).
|
||||
|
||||
If the file is generated in a location on your website that's publicly-visible, you should be able to subscribe to it from a calendar application. Then run the script periodically to have it update.
|
||||
|
||||
Note that the file might contain private checkins or information you don't want to be public. In which case, it's probably best to make the name of any such publicly-readable file very obscure.
|
||||
|
||||
To generate a `.kml` file, see the `kind` option below.
|
||||
|
||||
|
||||
### Script options
|
||||
|
||||
#### `--all`
|
||||
|
||||
By default the script only fetches the most recent 250 checkins. To fetch ALL checkins add the `--all` flag:
|
||||
Create a `.ics` file with your recent check-ins:
|
||||
|
||||
```bash
|
||||
$ ./generate_feeds.py --all
|
||||
uv run ./generate_feeds.py
|
||||
```
|
||||
|
||||
Depending on how many checkins you have you might only want to run it with
|
||||
`--all` the first time and, once that's imported into a calendar application,
|
||||
subsequently only fetch recent checkins.
|
||||
|
||||
### `-k` or `--kind`
|
||||
|
||||
By default the script generates an iCal `.ics` file. Or, use this option to
|
||||
specify an `.ics` file or a `.kml` file:
|
||||
Get all your check-ins (may take a while):
|
||||
|
||||
```bash
|
||||
$ ./generate_feeds.py -k ics
|
||||
$ ./generate_feeds.py -k kml
|
||||
$ ./generate_feeds.py --kind=ics
|
||||
$ ./generate_feeds.py --kind=kml
|
||||
uv run ./generate_feeds.py --all
|
||||
```
|
||||
|
||||
#### `-v` or `--verbose`
|
||||
### Sync to CalDAV
|
||||
|
||||
By default the script will only output text if something goes wrong. To get
|
||||
brief output use `-v` or `--verbose`:
|
||||
Upload check-ins directly to a CalDAV server:
|
||||
|
||||
```bash
|
||||
$ ./generate_feeds.py -v
|
||||
Fetched 250 checkins from the API
|
||||
Generated calendar file ./mycalendar.ics
|
||||
uv run ./generate_feeds.py --kind caldav
|
||||
```
|
||||
|
||||
If fetching `--all` checkins then increasing the verbosity with another `-v`
|
||||
will show more info than the above:
|
||||
Make sure your CalDAV settings are configured in `config.ini`.
|
||||
|
||||
## Configuration
|
||||
|
||||
The `config.ini` file supports these sections:
|
||||
|
||||
### [Foursquare]
|
||||
- `AccessToken`: Your Foursquare API access token
|
||||
|
||||
### [Local]
|
||||
- `IcsFilepath`: Path where the `.ics` file should be saved
|
||||
|
||||
### [CalDAV]
|
||||
- `url`: Your CalDAV server URL
|
||||
- `username`: CalDAV username
|
||||
- `password`: CalDAV password
|
||||
- `calendar_name`: Name of the calendar to create/use
|
||||
|
||||
## Command Line Options
|
||||
|
||||
### `--all`
|
||||
Fetch all check-ins instead of just the recent 250:
|
||||
```bash
|
||||
uv run ./generate_feeds.py --all
|
||||
```
|
||||
|
||||
### `--kind` / `-k`
|
||||
Specify output type (`ics` or `caldav`):
|
||||
```bash
|
||||
uv run ./generate_feeds.py --kind ics
|
||||
uv run ./generate_feeds.py --kind caldav
|
||||
```
|
||||
|
||||
### `--verbose` / `-v`
|
||||
Enable verbose output:
|
||||
```bash
|
||||
uv run ./generate_feeds.py -v # Basic info
|
||||
uv run ./generate_feeds.py -vv # Detailed progress (with --all)
|
||||
```
|
||||
|
||||
### `--config` / `-c`
|
||||
Specify a custom config file path:
|
||||
```bash
|
||||
uv run ./generate_feeds.py --config /path/to/config.ini
|
||||
uv run ./generate_feeds.py -c ./my-config.ini
|
||||
```
|
||||
|
||||
## Docker Usage
|
||||
|
||||
A pre-built Docker image is available at `ghcr.io/nikdoof/foursquare-feeds` for easy deployment and automation.
|
||||
|
||||
### Quick Start
|
||||
|
||||
Run with a local config file:
|
||||
|
||||
```bash
|
||||
$ ./generate_feeds.py -vv --all
|
||||
5746 checkins to fetch
|
||||
Fetched checkins 1-250
|
||||
Fetched checkins 251-500
|
||||
[etc...]
|
||||
Fetched checkins 5501-5750
|
||||
Fetched 5744 checkins from the API
|
||||
Generated calendar file ./mycalendar.ics
|
||||
docker run --rm \
|
||||
-v $(pwd)/config.ini:/app/config/config.ini:ro \
|
||||
ghcr.io/nikdoof/foursquare-feeds:latest \
|
||||
--kind ics
|
||||
```
|
||||
|
||||
(No I don't know why it fetched 2 fewer checkins than it says I have.)
|
||||
### Volume Mounts
|
||||
|
||||
The container expects the config file at `/app/config/config.ini`. You can mount your config file using:
|
||||
|
||||
- **Bind mount**: `-v /host/path/config.ini:/app/config/config.ini:ro`
|
||||
- **Volume**: `-v config-volume:/app/config` (with config.ini in the volume)
|
||||
- **ConfigMap/Secret** (in Kubernetes)
|
||||
|
||||
### Container Arguments
|
||||
|
||||
The container accepts all the same arguments as the script:
|
||||
|
||||
```bash
|
||||
# Sync to CalDAV
|
||||
docker run --rm -v $(pwd)/config.ini:/app/config/config.ini:ro \
|
||||
ghcr.io/nikdoof/foursquare-feeds:latest --kind caldav
|
||||
|
||||
# Fetch all check-ins with verbose output
|
||||
docker run --rm -v $(pwd)/config.ini:/app/config/config.ini:ro \
|
||||
ghcr.io/nikdoof/foursquare-feeds:latest --all -v
|
||||
```
|
||||
|
||||
### Kubernetes CronJob Example
|
||||
|
||||
For automated syncing, you can deploy as a Kubernetes CronJob:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: foursquare-feeds
|
||||
namespace: jobs
|
||||
spec:
|
||||
schedule: "*/5 * * * *" # Every 5 minutes
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: foursquare-feeds
|
||||
image: ghcr.io/nikdoof/foursquare-feeds:latest
|
||||
args: ["-k", "caldav"]
|
||||
volumeMounts:
|
||||
- name: foursquare-feeds-config
|
||||
mountPath: /app/config/config.ini
|
||||
subPath: config.ini
|
||||
restartPolicy: OnFailure
|
||||
volumes:
|
||||
- name: foursquare-feeds-config
|
||||
secret:
|
||||
secretName: foursquare-feeds-config
|
||||
```
|
||||
|
||||
This example:
|
||||
- Runs every 5 minutes
|
||||
- Syncs check-ins to CalDAV
|
||||
- Uses a Kubernetes Secret for configuration
|
||||
- Stores the config as `config.ini` in the secret
|
||||
|
||||
## What Gets Exported
|
||||
|
||||
Each check-in becomes a calendar event with:
|
||||
|
||||
- **Title**: "@ [Venue Name]"
|
||||
- **Location**: Venue name and address
|
||||
- **Time**: 15-minute event starting at check-in time
|
||||
- **Description**: Your shout/comment, plus metadata like:
|
||||
- Days since last visit
|
||||
- Mayor status at the time
|
||||
- **URL**: Link to the check-in on Foursquare
|
||||
|
||||
## Privacy Considerations
|
||||
|
||||
- Check-ins may contain private information
|
||||
- If hosting `.ics` files publicly, use obscure filenames
|
||||
- Consider filtering private check-ins before sharing
|
||||
|
||||
## About
|
||||
|
||||
By Phil Gyford
|
||||
phil@gyford.com
|
||||
https://www.gyford.com
|
||||
https://github.com/philgyford/foursquare-feeds
|
||||
**Original Author**: Phil Gyford
|
||||
**Repository**: https://github.com/philgyford/foursquare-feeds
|
||||
|
||||
This tool exists because Foursquare's [official feeds](https://foursquare.com/feeds/) stopped working reliably. [Read more about the original motivation of Phil to create the tool](https://www.gyford.com/phil/writing/2019/05/13/foursquare-swarm-ical-feed/).
|
||||
|
||||
@@ -8,5 +8,4 @@ password=xxxxx
|
||||
calendar_name=Swarm Check-Ins
|
||||
|
||||
[Local]
|
||||
IcsFilepath=./foursquare.ics
|
||||
KmlFilepath=./foursquare.kml
|
||||
IcsFilepath=./foursquare.ics
|
||||
@@ -4,7 +4,6 @@ import configparser
|
||||
import logging
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from xml.sax.saxutils import escape as xml_escape
|
||||
|
||||
import arrow
|
||||
import caldav
|
||||
@@ -16,19 +15,19 @@ current_dir = os.path.realpath(os.path.dirname(__file__))
|
||||
CONFIG_FILE = os.path.join(current_dir, "config.ini")
|
||||
|
||||
# The kinds of file we can generate:
|
||||
VALID_KINDS = ["ics", "kml", "caldav"]
|
||||
VALID_KINDS = ["ics", "caldav"]
|
||||
|
||||
|
||||
class FeedGenerator:
|
||||
fetch = "recent"
|
||||
|
||||
def __init__(self, fetch="recent"):
|
||||
def __init__(self, config_file=CONFIG_FILE, fetch="recent"):
|
||||
"Loads config, sets up Foursquare API client."
|
||||
|
||||
self.fetch = fetch
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
self._load_config(CONFIG_FILE)
|
||||
self._load_config(config_file)
|
||||
|
||||
self.client = foursquare.Foursquare(access_token=self.api_access_token)
|
||||
|
||||
@@ -44,7 +43,6 @@ class FeedGenerator:
|
||||
|
||||
self.api_access_token = config.get("Foursquare", "AccessToken")
|
||||
self.ics_filepath = config.get("Local", "IcsFilepath")
|
||||
self.kml_filepath = config.get("Local", "KmlFilepath")
|
||||
self.caldav_url = config.get("CalDAV", "url", fallback=None)
|
||||
self.caldav_username = config.get("CalDAV", "username", fallback=None)
|
||||
self.caldav_password = config.get("CalDAV", "password", fallback=None)
|
||||
@@ -64,11 +62,12 @@ class FeedGenerator:
|
||||
self.logger.info("Fetched {} checkin{} from the API".format(len(checkins), plural))
|
||||
|
||||
if kind == "ics":
|
||||
filepath = self._generate_ics_file(checkins)
|
||||
elif kind == "kml":
|
||||
filepath = self._generate_kml_file(checkins)
|
||||
calendar = self._generate_calendar(checkins)
|
||||
|
||||
self.logger.info("Generated file {}".format(filepath))
|
||||
with open(self.ics_filepath, "w") as f:
|
||||
f.writelines(calendar)
|
||||
|
||||
self.logger.info("Generated file {}".format(self.ics_filepath))
|
||||
|
||||
def _get_recent_checkins(self) -> list:
|
||||
"Make one request to the API for the most recent checkins."
|
||||
@@ -167,7 +166,7 @@ class FeedGenerator:
|
||||
continue
|
||||
|
||||
venue_name = checkin["venue"]["name"]
|
||||
tz_offset = self._get_checkin_timezone(checkin)
|
||||
tz_offset = tzoffset(None, checkin["timeZoneOffset"] * 60)
|
||||
|
||||
e = Event()
|
||||
start = arrow.get(checkin["createdAt"]).replace(tzinfo=tz_offset)
|
||||
@@ -209,91 +208,6 @@ class FeedGenerator:
|
||||
|
||||
return c
|
||||
|
||||
def _generate_kml_file(self, checkins):
|
||||
"""Supplied with a list of checkin data from the API, generates
|
||||
and saves a kml file.
|
||||
|
||||
Returns the filepath of the saved file.
|
||||
|
||||
Keyword arguments:
|
||||
checkins -- A list of dicts, each one data about a single checkin.
|
||||
"""
|
||||
import simplekml
|
||||
|
||||
user = self._get_user()
|
||||
|
||||
kml = simplekml.Kml()
|
||||
|
||||
# The original Foursquare files had a Folder with name and
|
||||
# description like this, so:
|
||||
names = [user.get("firstName", ""), user.get("lastName", "")]
|
||||
user_name = " ".join(names).strip()
|
||||
name = "foursquare checkin history for {}".format(user_name)
|
||||
fol = kml.newfolder(name=name, description=name)
|
||||
|
||||
for checkin in checkins:
|
||||
if "venue" not in checkin:
|
||||
# I had some checkins with no data other than
|
||||
# id, createdAt and source.
|
||||
continue
|
||||
|
||||
venue_name = checkin["venue"]["name"]
|
||||
tz_offset = self._get_checkin_timezone(checkin)
|
||||
url = "https://foursquare.com/v/{}".format(checkin["venue"]["id"])
|
||||
|
||||
description = ['@<a href="{}">{}</a>'.format(url, venue_name)]
|
||||
if "shout" in checkin and len(checkin["shout"]) > 0:
|
||||
description.append('"{}"'.format(checkin["shout"]))
|
||||
description.append("Timezone offset: {}".format(tz_offset))
|
||||
|
||||
coords = [
|
||||
(
|
||||
checkin["venue"]["location"]["lng"],
|
||||
checkin["venue"]["location"]["lat"],
|
||||
)
|
||||
]
|
||||
|
||||
visibility = 0 if "private" in checkin else 1
|
||||
|
||||
pnt = fol.newpoint(
|
||||
name=venue_name,
|
||||
description="<![CDATA[{}]]>".format("\n".join(description)),
|
||||
coords=coords,
|
||||
visibility=visibility,
|
||||
# Both of these were set like this in Foursquare's original KML:
|
||||
altitudemode=simplekml.AltitudeMode.relativetoground,
|
||||
extrude=1,
|
||||
)
|
||||
|
||||
# Foursquare's KML feeds had 'updated' and 'published' elements
|
||||
# in the Placemark, but I don't *think* those are standard, so:
|
||||
pnt.timestamp.when = arrow.get(
|
||||
checkin["createdAt"],
|
||||
tzinfo=self._get_checkin_timezone(checkin),
|
||||
).isoformat()
|
||||
|
||||
# Use the address, if any:
|
||||
if "location" in checkin["venue"]:
|
||||
loc = checkin["venue"]["location"]
|
||||
if "formattedAddress" in loc and len(loc["formattedAddress"]) > 0:
|
||||
address = ", ".join(loc["formattedAddress"])
|
||||
# While simplexml escapes other strings, it threw a wobbly
|
||||
# over '&' in addresses, so escape them:
|
||||
pnt.address = xml_escape(address)
|
||||
|
||||
kml.save(self.kml_filepath)
|
||||
|
||||
return self.kml_filepath
|
||||
|
||||
def _get_checkin_timezone(self, checkin):
|
||||
"""Given a checkin from the API, returns an arrow timezone object
|
||||
representing the timezone offset of that checkin.
|
||||
|
||||
Keyword arguments
|
||||
checkin -- A dict of data about a single checkin
|
||||
"""
|
||||
return tzoffset(None, checkin["timeZoneOffset"] * 60)
|
||||
|
||||
def sync_calendar_to_caldav(self):
|
||||
"""
|
||||
Syncs all events from the generated calendar to a CalDAV server.
|
||||
@@ -328,29 +242,9 @@ class FeedGenerator:
|
||||
cal = principal.make_calendar(name=self.caldav_calendar_name)
|
||||
|
||||
self.logger.debug("Calendar has {} events".format(len(calendar.events)))
|
||||
|
||||
# Upload each event from the ics.Calendar object
|
||||
for event in calendar.events:
|
||||
# Each event must have a unique UID
|
||||
# Use the event UID if present, otherwise generate a deterministic one from checkin ID
|
||||
if not event.uid:
|
||||
# Try to extract checkin ID from event.url or event.name as fallback
|
||||
checkin_id = None
|
||||
if hasattr(event, "url") and event.url:
|
||||
# URL format: .../checkin/<checkin_id>
|
||||
parts = event.url.rstrip("/").split("/")
|
||||
if "checkin" in parts:
|
||||
idx = parts.index("checkin")
|
||||
if idx + 1 < len(parts):
|
||||
checkin_id = parts[idx + 1]
|
||||
if not checkin_id and hasattr(event, "uid") and event.uid:
|
||||
# fallback: try to parse from event.uid
|
||||
if "@" in event.uid:
|
||||
checkin_id = event.uid.split("@")[0]
|
||||
if not checkin_id:
|
||||
# fallback: use event.name
|
||||
checkin_id = event.name
|
||||
# Generate a repeatable UID using a namespace and checkin_id
|
||||
event.uid = "{}@foursquare.com".format(checkin_id)
|
||||
self.logger.debug("Uploading event with UID: {}".format(event.uid))
|
||||
cal.add_event(event.serialize())
|
||||
|
||||
@@ -375,7 +269,7 @@ def main():
|
||||
"-k",
|
||||
"--kind",
|
||||
action="store",
|
||||
help="Either ics, kml, or caldav. Default is ics.",
|
||||
help="Either ics, or caldav. Default is ics.",
|
||||
choices=VALID_KINDS,
|
||||
default="ics",
|
||||
required=False,
|
||||
@@ -389,6 +283,14 @@ def main():
|
||||
help="-v or --verbose for brief output; -vv for more.",
|
||||
required=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--config",
|
||||
action="store",
|
||||
help="Path to config file. Default is 'config.ini' in the current directory.",
|
||||
required=False,
|
||||
default=CONFIG_FILE,
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -403,7 +305,7 @@ def main():
|
||||
else:
|
||||
to_fetch = "recent"
|
||||
|
||||
generator = FeedGenerator(fetch=to_fetch)
|
||||
generator = FeedGenerator(config_file=args.config, fetch=to_fetch)
|
||||
|
||||
if args.kind == "caldav":
|
||||
generator.sync_calendar_to_caldav()
|
||||
@@ -411,8 +313,6 @@ def main():
|
||||
# Generate the requested kind of file
|
||||
generator.generate(kind=args.kind)
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
sys.exit(main())
|
||||
sys.exit(main() or 0)
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "foursquare-feeds"
|
||||
version = "2.3.1"
|
||||
version = "2.4.0"
|
||||
description = "A python script for generating an iCal feed from your Foursquare checkins"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
@@ -8,5 +8,11 @@ dependencies = [
|
||||
"caldav>=2.0.1",
|
||||
"foursquare",
|
||||
"ics",
|
||||
"simplekml",
|
||||
"requests>=2.32.4",
|
||||
"urllib3>=2.5.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ruff>=0.12.8",
|
||||
]
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv export --format requirements-txt --output-file requirements.txt
|
||||
arrow==1.3.0 \
|
||||
--hash=sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85 \
|
||||
--hash=sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80
|
||||
attrs==24.2.0 \
|
||||
--hash=sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346 \
|
||||
--hash=sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2
|
||||
certifi==2024.8.30 \
|
||||
--hash=sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9 \
|
||||
--hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8
|
||||
charset-normalizer==3.4.0 \
|
||||
--hash=sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e \
|
||||
--hash=sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6 \
|
||||
--hash=sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf \
|
||||
--hash=sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db \
|
||||
--hash=sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1 \
|
||||
--hash=sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03 \
|
||||
--hash=sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284 \
|
||||
--hash=sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15 \
|
||||
--hash=sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8 \
|
||||
--hash=sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2 \
|
||||
--hash=sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719 \
|
||||
--hash=sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631 \
|
||||
--hash=sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b \
|
||||
--hash=sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565 \
|
||||
--hash=sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7 \
|
||||
--hash=sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9 \
|
||||
--hash=sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114 \
|
||||
--hash=sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed \
|
||||
--hash=sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250 \
|
||||
--hash=sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920 \
|
||||
--hash=sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64 \
|
||||
--hash=sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23 \
|
||||
--hash=sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc \
|
||||
--hash=sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d \
|
||||
--hash=sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88 \
|
||||
--hash=sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90 \
|
||||
--hash=sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b \
|
||||
--hash=sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d \
|
||||
--hash=sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482 \
|
||||
--hash=sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67 \
|
||||
--hash=sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b \
|
||||
--hash=sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079
|
||||
foursquare==1!2020.1.30 \
|
||||
--hash=sha256:a9f27c3c426783a18ab96b1872345c9617a5513e0896c0bb4243840fab80eed3 \
|
||||
--hash=sha256:a5aa9c5b6609ea2610e929288fead0cf27a8bcea2141e2ae7289f6feb57d2041
|
||||
ics==0.7.2 \
|
||||
--hash=sha256:6743539bca10391635249b87d74fcd1094af20b82098bebf7c7521df91209f05 \
|
||||
--hash=sha256:5fcf4d29ec6e7dfcb84120abd617bbba632eb77b097722b7df70e48dbcf26103
|
||||
idna==3.10 \
|
||||
--hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
|
||||
--hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
|
||||
python-dateutil==2.9.0.post0 \
|
||||
--hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
|
||||
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
|
||||
pytz==2024.2 \
|
||||
--hash=sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a \
|
||||
--hash=sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725
|
||||
requests==2.32.3 \
|
||||
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
|
||||
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
|
||||
simplekml==1.3.6 \
|
||||
--hash=sha256:cda687be2754395fcab664e908ebf589facd41e8436d233d2be37a69efb1c536
|
||||
six==1.16.0 \
|
||||
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
|
||||
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
|
||||
tatsu==5.12.2 \
|
||||
--hash=sha256:5894dc7ddba9a1886a95ff2f06cef1be2b3d3a37c776eba8177ef4dcd80ccb03 \
|
||||
--hash=sha256:9c313186ae5262662cb3fbec52c9a12db1ef752e615f46cac3eb568cb91eacf9
|
||||
types-python-dateutil==2.9.0.20241003 \
|
||||
--hash=sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446 \
|
||||
--hash=sha256:250e1d8e80e7bbc3a6c99b907762711d1a1cdd00e978ad39cb5940f6f0a87f3d
|
||||
urllib3==2.2.3 \
|
||||
--hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9 \
|
||||
--hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac
|
||||
53
uv.lock
generated
53
uv.lock
generated
@@ -123,13 +123,19 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "foursquare-feeds"
|
||||
version = "2.3.0"
|
||||
version = "2.4.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "caldav" },
|
||||
{ name = "foursquare" },
|
||||
{ name = "ics" },
|
||||
{ name = "simplekml" },
|
||||
{ name = "requests" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -137,9 +143,13 @@ requires-dist = [
|
||||
{ name = "caldav", specifier = ">=2.0.1" },
|
||||
{ name = "foursquare" },
|
||||
{ name = "ics" },
|
||||
{ name = "simplekml" },
|
||||
{ name = "requests", specifier = ">=2.32.4" },
|
||||
{ name = "urllib3", specifier = ">=2.5.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "ruff", specifier = ">=0.12.8" }]
|
||||
|
||||
[[package]]
|
||||
name = "icalendar"
|
||||
version = "6.3.1"
|
||||
@@ -247,7 +257,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.3"
|
||||
version = "2.32.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
@@ -255,16 +265,35 @@ dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simplekml"
|
||||
version = "1.3.6"
|
||||
name = "ruff"
|
||||
version = "0.12.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/e4/c333a93b7e3346437ad1ff42b8e362b853eb405ad6243ab6163f9af2a460/simplekml-1.3.6.tar.gz", hash = "sha256:cda687be2754395fcab664e908ebf589facd41e8436d233d2be37a69efb1c536", size = 52999, upload-time = "2021-09-16T17:08:27.038Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745, upload-time = "2025-08-07T19:05:18.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162, upload-time = "2025-08-07T19:05:34.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382, upload-time = "2025-08-07T19:05:38.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482, upload-time = "2025-08-07T19:05:40.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload-time = "2025-08-07T19:05:42.866Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
@@ -304,11 +333,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.2.3"
|
||||
version = "2.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677, upload-time = "2024-09-12T10:52:18.401Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338, upload-time = "2024-09-12T10:52:16.589Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user