Skip to content

Commit

Permalink
Merge pull request #107 from ptmminh/feat/club
Browse files Browse the repository at this point in the history
feat: extract payments on club endpoint; feat: download attendance report; feat: update user's response to an event
  • Loading branch information
elliot-100 authored May 31, 2024
2 parents 6b4a9fc + ed616e3 commit eb9a9bd
Show file tree
Hide file tree
Showing 9 changed files with 333 additions and 69 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/

# VSC
.vscode/
########################################################################################

# Project specific
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ Get all your messages.
Send a message with content `text`.
Either specify an existing `chat_id`, or both `user` and `group_uid` for a new chat.

### get_event_attendance_xlsx()
Get Excel attendance report for a single event, available via the web client.

### change_response()
Change a member's response for an event (e.g. accept/decline)

## Example scripts

The following scripts are included as examples. Some of the scripts might require additional packages to be installed (csv, ical etc).
Expand All @@ -72,6 +78,9 @@ Generates a json-file for each group you are a member of.
### attendance.py <-f from_date> <-t to_date> [-a]
Generates a csv-file for each event between `from_date` and `to_date` with attendance status of all organizers. The optional parameter `-a` also includes all members that has been invited.

### transactions.py
Generates a csv-file for transactions / payments appeared in [Spond Club](https://www.spond.com/spond-club-overview/) > Finance > Payments.

## AsyncIO
[Asyncio](https://docs.python.org/3/library/asyncio.html) might seem intimidating in the beginning, but for basic stuff, it is quite easy to follow the examples above, and just remeber to prefix functions that use the API with `async def ...` and to `await` all API-calls and all calls to said functions.

Expand Down
1 change: 1 addition & 0 deletions config.py.sample
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
username = 'user@name.invalid'
password = 'Pa55w0rd'
club_id = '1234567890'
34 changes: 32 additions & 2 deletions manual_test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
Doesn't yet use `get_person(user)` or any `send_`, `update_` methods."""

import asyncio
import tempfile

from config import password, username
from spond import spond
from config import club_id, password, username
from spond import club, spond

DUMMY_ID = "DUMMY_ID"

Expand Down Expand Up @@ -40,8 +41,28 @@ async def main() -> None:
for i, message in enumerate(messages):
print(f"[{i}] {_message_summary(message)}")

# ATTENDANCE EXPORT

print("\nGetting attendance report for the first event...")
e = events[0]
data = await s.get_event_attendance_xlsx(e["id"])
with tempfile.NamedTemporaryFile(
mode="wb", suffix=".xlsx", delete=False
) as temp_file:
temp_file.write(data)
print(f"Check out {temp_file.name}")

await s.clientsession.close()

# SPOND CLUB
sc = club.SpondClub(username=username, password=password)
print("\nGetting up to 10 transactions...")
transactions = await sc.get_transactions(club_id=club_id, max_items=10)
print(f"{len(transactions)} transactions:")
for i, t in enumerate(transactions):
print(f"[{i}] {_transaction_summary(t)}")
await sc.clientsession.close()


def _group_summary(group) -> str:
return f"id='{group['id']}', " f"name='{group['name']}'"
Expand All @@ -63,6 +84,15 @@ def _message_summary(message) -> str:
)


def _transaction_summary(transaction) -> str:
return (
f"id='{transaction['id']}', "
f"timestamp='{transaction['paidAt']}', "
f"payment_name='{transaction['paymentName']}', "
f"name={transaction['paidByName']}"
)


def _abbreviate(text, length) -> str:
"""Abbreviate long text, normalising line endings to escape characters."""
escaped_text = repr(text)
Expand Down
45 changes: 45 additions & 0 deletions spond/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from abc import ABC

import aiohttp


class AuthenticationError(Exception):
pass


class _SpondBase(ABC):
def __init__(self, username, password, api_url):
self.username = username
self.password = password
self.api_url = api_url
self.clientsession = aiohttp.ClientSession(cookie_jar=aiohttp.CookieJar())
self.token = None

@property
def auth_headers(self):
return {
"content-type": "application/json",
"Authorization": f"Bearer {self.token}",
}

def require_authentication(func: callable):
async def wrapper(self, *args, **kwargs):
if not self.token:
try:
await self.login()
except AuthenticationError as e:
await self.clientsession.close()
raise e
return await func(self, *args, **kwargs)

return wrapper

async def login(self):
login_url = f"{self.api_url}login"
data = {"email": self.username, "password": self.password}
async with self.clientsession.post(login_url, json=data) as r:
login_result = await r.json()
self.token = login_result.get("loginToken")
if self.token is None:
err_msg = f"Login failed. Response received: {login_result}"
raise AuthenticationError(err_msg)
58 changes: 58 additions & 0 deletions spond/club.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from typing import Optional

from .base import _SpondBase


class SpondClub(_SpondBase):
def __init__(self, username, password):
super().__init__(username, password, "https://api.spond.com/club/v1/")
self.transactions = None

@_SpondBase.require_authentication
async def get_transactions(
self, club_id: str, skip: Optional[int] = None, max_items: int = 100
):
"""
Retrieves a list of transactions/payments for a specified club.
Parameters
----------
club_id : str
Identifier for the club. Note that this is different from the Group ID used
in the core API.
max_items : int, optional
The maximum number of transactions to retrieve. Defaults to 100.
skip : int, optional
This endpoint only returns 25 transactions at a time (page scrolling).
Therefore, we need to increment this `skip` param to grab the next
25 etc. Defaults to None. It's better to keep `skip` at None
and specify `max_items` instead. This param is only here for the
recursion implementation
Returns
-------
list of dict
A list of transactions, each represented as a dictionary.
"""
if self.transactions is None:
self.transactions = []

url = f"{self.api_url}transactions"
params = None if skip is None else {"skip": skip}
headers = {**self.auth_headers, "X-Spond-Clubid": club_id}

async with self.clientsession.get(url, headers=headers, params=params) as r:
if r.status == 200:
t = await r.json()
if len(t) == 0:
return self.transactions

self.transactions.extend(t)
if len(self.transactions) < max_items:
return await self.get_transactions(
club_id=club_id,
skip=len(t) if skip is None else skip + len(t),
max_items=max_items,
)

return self.transactions
Loading

0 comments on commit eb9a9bd

Please sign in to comment.