Skip to content

Commit 649c1ba

Browse files
committed
First success with firestore library
1 parent aedbff5 commit 649c1ba

8 files changed

Lines changed: 235 additions & 64 deletions

File tree

README.md

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,15 @@ No guarantees are provided here. If you wanna use it, go for it, but do know tha
1717
To set up a development environment:
1818

1919
1. Ensure you have Python 3.7+ installed.
20-
2. Create a virtual environment:
20+
2. setup the virtual environment:
2121
```bash
22+
# Create a virtual environment
2223
python3 -m venv .venv
23-
```
24-
3. Activate the virtual environment:
25-
- On Linux/macOS:
26-
```bash
27-
source .venv/bin/activate
28-
```
29-
- On Windows:
30-
```cmd
31-
.venv\Scripts\activate
32-
```
33-
4. Install dependencies:
34-
```bash
24+
# Activate the virtual environment
25+
source .venv/bin/activate
26+
# Install dependencies
3527
pip install -r requirements.txt
36-
```
37-
5. Install the package in editable (dev) mode:
38-
```bash
28+
# Install the package in editable (dev) mode
3929
pip install -e .
4030
```
4131

requirements.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
1-
requests==2.32.4
1+
requests
22
python-dotenv
3+
#pyrebase4
4+
#urllib3
5+
google-cloud-firestore
6+
google-auth

run.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
from dotenv import load_dotenv
3-
from spendee import Spendee
3+
from spendee import SpendeeApi
4+
from spendee import SpendeeFirestore
45

56
# Load environment variables from .env file
67
load_dotenv()
@@ -12,13 +13,17 @@
1213
print('Please set EMAIL and PASSWORD in your .env file.')
1314
exit(1)
1415

15-
spendee = Spendee(EMAIL, PASSWORD)
16+
# spendee = SpendeeApi(EMAIL, PASSWORD)
1617

1718

18-
accounts = spendee.wallet_get_all()
19-
print('Available accounts:')
20-
for acc in accounts:
21-
print(f"- {acc.get('name', 'Unnamed')} (ID: {acc.get('id')}, Balance: {acc.get('balance')} {acc.get('currency')})")
19+
# accounts = spendee.wallet_get_all()
20+
# print('Available accounts:')
21+
# for acc in accounts:
22+
# print(f"- {acc.get('name', 'Unnamed')} (ID: {acc.get('id')}, Balance: {acc.get('balance')} {acc.get('currency')})")
23+
24+
spendee = SpendeeFirestore(EMAIL, PASSWORD)
25+
26+
print(spendee.example_call())
2227

2328
# type = 'bank_account'
2429
# banks_get_all

spendee/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
from .spendee import Spendee
1+
from .spendee_api import SpendeeApi
2+
from .spendee_firestore import SpendeeFirestore

spendee/firebase_client.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import requests
2+
from requests import Session
3+
4+
class FirebaseClient(Session):
5+
def __init__(self, email, password, google_client_id: str = 'AIzaSyCCJPDxVNVFEARQ-LxH7q2aZtdQJGGFO84'):
6+
self.email = email
7+
self.password = password
8+
self.google_client_id = google_client_id
9+
self._access_token = None
10+
self._refresh_token = None
11+
self._device_uuid = None
12+
super().__init__()
13+
14+
def authenticate(self):
15+
"""
16+
Handles the full authentication flow: gets refresh token, access token, and device UUID.
17+
"""
18+
self._refresh_token = self.get_refresh_token()
19+
self._access_token = self.get_access_token()
20+
self._device_uuid = self.get_device_uuid_from_login()
21+
22+
def get_refresh_token(self):
23+
url = f'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key={self.google_client_id}'
24+
payload = {
25+
'email': self.email,
26+
'password': self.password,
27+
'returnSecureToken': True,
28+
}
29+
response = requests.post(url, json=payload)
30+
response.raise_for_status()
31+
data = response.json()
32+
return data['refreshToken']
33+
34+
def get_access_token(self):
35+
url = f'https://securetoken.googleapis.com/v1/token?key={self.google_client_id}'
36+
payload = {
37+
'refresh_token': self._refresh_token,
38+
'grant_type': 'refresh_token',
39+
}
40+
response = requests.post(url, json=payload)
41+
response.raise_for_status()
42+
data = response.json()
43+
#print("Access Token:", data['access_token']) # Debugging line to check the access token
44+
return data['access_token']
45+
46+
def get_device_uuid_from_login(self):
47+
# This should be implemented in the child class if needed, as it may require a specific API call
48+
return None
49+
50+
@property
51+
def access_token(self):
52+
if not self._access_token:
53+
self.authenticate()
54+
return self._access_token
55+
56+
@property
57+
def device_uuid(self):
58+
return self._device_uuid
59+
60+
@device_uuid.setter
61+
def device_uuid(self, value):
62+
self._device_uuid = value

spendee/prompts.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# discussions
2+
3+
firebase SDK authentication:
4+
- https://claude.ai/share/74c50295-ffad-4659-bd21-0c5b3810b9d6
5+
6+
# Context cache
7+
---
8+
I am a Spendee user, which is a webpage and app on mobile, to track financial transactions gathered from multiple bank accounts.
9+
They do not support a documented API, but I want to implement an MCP server to enable an AI agent to work collaboratively fetching data or refining transaction categories or descriptions.
10+
For that I explored an old github repo, which implemented authentication and REST API calls available back then.
11+
Unfortunately those API calls return outdated data somewhy, like bank account data is 1 week older, than what is available on the webpage.
12+
So I started to investigate the web traffic of the webpage and explored that it uses firestore.
13+
I never used firestore before, so I need explanations and guidance throghout our work.
14+
The client I write will be implemented in python.
15+
---
16+
Here are some key code snippets to understand some aspects of it:
17+
18+
Config identified from web traffic:
19+
```json
20+
"google_client_id": "AIzaSyCCJPDxVNVFEARQ-LxH7q2aZtdQJGGFO84",
21+
"config": {
22+
"apiKey": "AIzaSyCCJPDxVNVFEARQ-LxH7q2aZtdQJGGFO84",
23+
"authDomain": "spendee-app.firebaseapp.com",
24+
"databaseURL": "https://spendee-app.firebaseio.com",
25+
"projectId": "spendee-app",
26+
"storageBucket": "spendee-app.appspot.com",
27+
"messagingSenderId": "320191066468",
28+
"appId": "1:320191066468:web:b91a0a66620b3912",
29+
"measurementId": "G-6XWQZRWRLK"
30+
}
31+
```
32+
33+
Working authentication layer extracted from former open source project:
34+
```python
35+
from requests import Session
36+
37+
class FirebaseClient(Session):
38+
def __init__(self, email, password, google_client_id: str = 'AIzaSyCCJPDxVNVFEARQ-LxH7q2aZtdQJGGFO84'):
39+
self.email = email
40+
self.password = password
41+
self.google_client_id = google_client_id
42+
self._access_token = None
43+
self._refresh_token = None
44+
self._device_uuid = None
45+
super().__init__()
46+
47+
def authenticate(self):
48+
"""
49+
Handles the full authentication flow: gets refresh token, access token, and device UUID.
50+
"""
51+
self._refresh_token = self.get_refresh_token()
52+
self._access_token = self.get_access_token()
53+
self._device_uuid = self.get_device_uuid_from_login()
54+
55+
def get_refresh_token(self):
56+
url = f'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key={self.google_client_id}'
57+
payload = {
58+
'email': self.email,
59+
'password': self.password,
60+
'returnSecureToken': True,
61+
}
62+
response = requests.post(url, json=payload)
63+
response.raise_for_status()
64+
data = response.json()
65+
return data['refreshToken']
66+
67+
def get_access_token(self):
68+
url = f'https://securetoken.googleapis.com/v1/token?key={self.google_client_id}'
69+
payload = {
70+
'refresh_token': self._refresh_token,
71+
'grant_type': 'refresh_token',
72+
}
73+
response = requests.post(url, json=payload)
74+
response.raise_for_status()
75+
data = response.json()
76+
print("Access Token:", data['access_token']) # Debugging line to check the access token
77+
return data['access_token']
78+
79+
def get_device_uuid_from_login(self):
80+
# This should be implemented in the child class if needed, as it may require a specific API call
81+
return None
82+
```
83+
84+
Example firestore path extracted from web traffic:
85+
86+
projects/spendee-app/databases/(default)/documents/users/af150597-5b96-4f8b-9eb3-0b2e0aa72a81/wallets/b368c5c2-68fe-4f98-9d4f-08e0cdca57a7/transactions/26624eef-6956-4cbc-8aee-68e4fb97396b
87+
Lines changed: 25 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,15 @@
11
from uuid import uuid4
22
import datetime
3-
4-
import requests
5-
from requests import Session
3+
from .firebase_client import FirebaseClient
64
from requests.exceptions import RequestException
75

86
from .exceptions import SpendeeError
97

108

11-
class Spendee(Session):
9+
class SpendeeApi(FirebaseClient):
1210
def __init__(self, email: str, password: str, base_url: str = 'https://api.spendee.com/', google_client_id: str = 'AIzaSyCCJPDxVNVFEARQ-LxH7q2aZtdQJGGFO84'):
13-
"""
14-
:param email: user email to use for login
15-
:param password: user password to use for login
16-
:param base_url: base URL of the API
17-
"""
1811
self.base_url = base_url
19-
self._email = email
20-
self._password = password
21-
self._google_client_id = google_client_id
22-
23-
self._access_token = None
24-
self._device_uuid = None
25-
super(Spendee, self).__init__()
12+
super().__init__(email, password, google_client_id)
2613

2714
def _build_url(self, version: str, url: str):
2815
"""
@@ -75,19 +62,20 @@ def request(self, method, url, version: str = 'v1', headers=None, params=None, *
7562
'Spendee-Version': 'master'
7663
}
7764

65+
# Ensure authentication
7866
if not self._access_token and not any(i in url for i in ('user-registration', 'googleapis')):
79-
self.user_login()
67+
self.authenticate()
8068

8169
if self._access_token:
82-
headers['Authorization'] = 'Bearer {}'.format(self._access_token)
70+
headers['Authorization'] = f'Bearer {self._access_token}'
8371
if self._device_uuid:
8472
headers['Device-Uuid'] = self._device_uuid
8573

8674
url = self._build_url(version, url)
8775

8876
response = None
8977
try:
90-
response = super(Spendee, self).request(method=method, url=url, headers=headers, params=params, **kwargs)
78+
response = super(SpendeeApi, self).request(method=method, url=url, headers=headers, params=params, **kwargs)
9179
response.raise_for_status()
9280
except RequestException as e:
9381
raise SpendeeError("Spendee returned a non-200 HTTP code.", response=response) from e
@@ -106,6 +94,20 @@ def request(self, method, url, version: str = 'v1', headers=None, params=None, *
10694
else:
10795
return result
10896

97+
def get_device_uuid_from_login(self):
98+
# Actually perform the login to get device_uuid
99+
url = self._build_url('v3', 'auth/login')
100+
payload = {
101+
"global_currency": "USD",
102+
"default_wallet_name": "Cash Wallet",
103+
"timezone": 'Asia/Jakarta',
104+
"platform": "web",
105+
"version": "master",
106+
"credential": None
107+
}
108+
response = super().post(url=url, json=payload)
109+
return response.get('device_uuid')
110+
109111
###
110112

111113
def user_registration(self, email: str = None, password: str = None, device_uuid: str = None,
@@ -172,26 +174,9 @@ def user_registration(self, email: str = None, password: str = None, device_uuid
172174
"device_uuid": device_uuid
173175
}
174176

175-
return super(Spendee, self).post(url=url, version=version, **kwargs)
177+
return super(SpendeeApi, self).post(url=url, version=version, **kwargs)
176178

177-
def _get_refresh_token(self, email: str = None, password: str = None, **kwargs):
178-
kwargs['json'] = {
179-
'email': email,
180-
'password': password,
181-
'returnSecureToken': True,
182-
}
183-
url = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key={}'.format(self._google_client_id)
184-
response = super(Spendee, self).post(url=url, **kwargs)
185-
return response['refreshToken']
186-
187-
def _get_access_token(self, refresh_token: str, **kwargs):
188-
kwargs['json'] = {
189-
'refresh_token': refresh_token,
190-
'grant_type': 'refresh_token',
191-
}
192-
url = 'https://securetoken.googleapis.com/v1/token?key={}'.format(self._google_client_id)
193-
response = super(Spendee, self).post(url=url, **kwargs)
194-
return response['access_token']
179+
###
195180

196181
def user_login(self, version: str = 'v3', url: str = 'auth/login', **kwargs):
197182
"""
@@ -211,7 +196,7 @@ def user_login(self, version: str = 'v3', url: str = 'auth/login', **kwargs):
211196
"version": "master",
212197
"credential": None
213198
}
214-
result = super(Spendee, self).post(url=url, version=version, **kwargs)
199+
result = super(SpendeeApi, self).post(url=url, version=version, **kwargs)
215200
self._device_uuid = result['device_uuid']
216201

217202
def user_logout(self, version: str = 'v1.4', url: str = 'user-logout', **kwargs):
@@ -221,7 +206,7 @@ def user_logout(self, version: str = 'v1.4', url: str = 'user-logout', **kwargs)
221206
:rtype: bool
222207
:return: returns True if logout was successful
223208
"""
224-
return super(Spendee, self).post(url=url, version=version, **kwargs)
209+
return super(SpendeeApi, self).post(url=url, version=version, **kwargs)
225210

226211
def user_get_profile(self, version: str = 'v1.4', url: str = 'user-get-profile', **kwargs):
227212
"""

spendee/spendee_firestore.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from google.auth.credentials import Credentials
2+
from google.cloud import firestore
3+
from .firebase_client import FirebaseClient
4+
5+
class CustomFirebaseCredentials(Credentials):
6+
"""Custom credentials that use existing Firebase access token"""
7+
8+
def __init__(self, access_token):
9+
super().__init__()
10+
self.token = access_token
11+
12+
def refresh(self, request):
13+
# todo
14+
pass
15+
16+
def expired(self):
17+
# Check if token is expired
18+
pass
19+
20+
class SpendeeFirestore(FirebaseClient):
21+
def __init__(self, email: str, password: str, base_url: str = 'https://api.spendee.com/', google_client_id: str = 'AIzaSyCCJPDxVNVFEARQ-LxH7q2aZtdQJGGFO84'):
22+
self.base_url = base_url
23+
super().__init__(email, password, google_client_id)
24+
self.authenticate()
25+
self.credentials = CustomFirebaseCredentials(self.get_access_token())
26+
self.client = firestore.Client(project="spendee-app", credentials=self.credentials)
27+
28+
def example_call(self):
29+
# example = self.client\
30+
# .collection('users')\
31+
# .document('af150597-5b96-4f8b-9eb3-0b2e0aa72a81')\
32+
# .collection('wallets')\
33+
# .document('b368c5c2-68fe-4f98-9d4f-08e0cdca57a7')\
34+
# .collection('transactions')\
35+
# .document('26624eef-6956-4cbc-8aee-68e4fb97396b')
36+
example = self.client.document('users/af150597-5b96-4f8b-9eb3-0b2e0aa72a81/wallets/b368c5c2-68fe-4f98-9d4f-08e0cdca57a7/transactions/26624eef-6956-4cbc-8aee-68e4fb97396b')
37+
return example.get().to_dict()

0 commit comments

Comments
 (0)