Skip to content

Commit

Permalink
Merge pull request #18 from Trevypants/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
Trevypants authored Jun 7, 2024
2 parents f3de7c1 + 943efe7 commit ae845f0
Show file tree
Hide file tree
Showing 3 changed files with 280 additions and 32 deletions.
204 changes: 192 additions & 12 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,197 @@
# Welcome to MkDocs
# PyRevolut: A Revolut Business API Wrapper

For full documentation visit [mkdocs.org](https://www.mkdocs.org).
[![codecov](https://codecov.io/gh/Trevypants/pyrevolut/graph/badge.svg?token=55UY8J1YZM)](https://codecov.io/gh/Trevypants/pyrevolut)
[![PyPI Package latest release](https://img.shields.io/pypi/v/pyrevolut.svg?color=%2334D058&label=pypi%20package)](https://pypi.org/project/pyrevolut/)
[![Supported versions](https://img.shields.io/pypi/pyversions/pyrevolut)](https://pypi.org/project/pyrevolut/)
[![License](https://img.shields.io/pypi/l/pyrevolut)](LICENSE)
[![PyPI Package download count (per month)](https://img.shields.io/pypi/dm/pyrevolut)](https://pypi.org/project/pyrevolut/)
[![Black code style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Ruff code quality](https://img.shields.io/badge/code%20quality-Ruff-000000.svg)](https://docs.astral.sh/ruff/)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/Trevypants/pyrevolut/test_integration.yml?branch=develop)](https://github.com/Trevypants/pyrevolut/actions)

## Commands
`pyrevolut` is an un-official wrapper around the [Revolut Business API](https://developer.revolut.com/docs/business/business-api).

* `mkdocs new [dir-name]` - Create a new project.
* `mkdocs serve` - Start the live-reloading docs server.
* `mkdocs build` - Build the documentation site.
* `mkdocs -h` - Print help message and exit.
## Installation

## Project layout
```bash
pip install pyrevolut
```

mkdocs.yml # The configuration file.
docs/
index.md # The documentation homepage.
... # Other markdown pages, images and other files.
## Usage

### Basic Usage

```python
from pyrevolut.client import Client

CREDS_JSON_LOC = "path/to/creds.json"

client = Client(
creds_loc=CREDS_JSON_LOC,
sandbox=True,
)

# Initialize the client
client.open()

# List all accounts for the authenticated user
accounts = client.Accounts.get_all_accounts()

# Close the client
client.close()

# You can also use the client as a context manager
with Client(
creds_loc=CREDS_JSON_LOC,
sandbox=True
) as client:
accounts = client.Accounts.get_all_accounts()
```

### Advanced Usage

It is possible to use the client library asynchronously by using the `AsyncClient` object.

```python
import asyncio
from pyrevolut.client import AsyncClient

CREDS_JSON_LOC = "path/to/creds.json"

client = AsyncClient(
creds_loc=CREDS_JSON_LOC,
sandbox=True,
)

# Run without context manager
async def run():
await client.open()
accounts = await client.Accounts.get_all_accounts()
await client.close()
return accounts

# Run with context manager
async def run_context_manager():
async with client:
accounts = await client.Accounts.get_all_accounts()
return accounts

# List all accounts for the authenticated user
accounts = asyncio.run(run())
accounts_context_manager = asyncio.run(run_context_manager())

```

## Authentication

In order to make use of the Revolut Business API, you will need to go through several steps to authenticate your application. The basic guide can be found [here](https://developer.revolut.com/docs/guides/manage-accounts/get-started/make-your-first-api-request). We have provided a simple CLI tool to help you generate the necessary credentials. This tool follows the steps outlined in the guide.

```bash

pyrevolut auth-manual

```

or equivalently

```bash

python -m pyrevolut auth-manual

```

Upon completion, you will have a `.json` file that you can use to authenticate your application.

Alternatively, in the event that you already have all your credential information stored, you can simply create a `.json` file with the following structure:

```json
{
"certificate": {
"public": "public-certificate-base64-encoded",
"private": "private-key-base64-encoded",
"expiration_dt": "2500-01-01T00:00:00Z"
},
"client_assert_jwt": {
"jwt": "client-assertion-jwt",
"expiration_dt": "2500-01-01T00:00:00Z"
},
"tokens": {
"access_token": "access-token",
"refresh_token": "refresh-token",
"token_type": "bearer",
"access_token_expiration_dt": "2020-01-01T17:22:42.934699Z",
"refresh_token_expiration_dt": "2500-01-01T00:00:00Z"
}
}
```

## API Support Status

The wrapper currently supports the following APIs:

- [x] Accounts
- [x] Retrieve all accounts
- [x] Retrieve an account
- [x] Retrieve account's full bank details
- [ ] Cards (Live only)
- [ ] Retrieve a list of cards
- [ ] Create a card
- [ ] Retrieve card details
- [ ] Update card details
- [ ] Terminate a card
- [ ] Freeze a card
- [ ] Unfreeze a card
- [ ] Retrieve sensitive card details
- [x] Counterparties
- [x] Retrieve a list of counterparties
- [x] Retrieve a counterparty
- [x] Delete a counterparty
- [x] Create a counterparty (Personal)
- [x] Create a counterparty (Business)
- [x] Validate an account name (CoP)
- [x] Foreign exchange
- [x] Get an exchange rate
- [x] Exchange money
- [ ] Payment drafts
- [x] Retrieve all payments drafts
- [ ] Create a payment draft
- [x] Retrieve a payment draft
- [ ] Delete a payment draft
- [x] Payout links
- [x] Retrieve a list of payout links
- [x] Retrieve a payout link
- [x] Create a payout link
- [x] Cancel a payout link
- [x] Simulations (Sandbox only)
- [x] Simulate a transfer state update
- [x] Simulate an account top-up
- [ ] Team members (Live only)
- [ ] Retrieve a list of team members
- [ ] Invite a new memebr to your business
- [ ] Retrieve team roles
- [x] Transactions
- [x] Retrieve a list of transactions
- [x] Retrieve a transaction
- [x] Transfers
- [x] Move money between your accounts
- [x] Create a transfer to another account
- [x] Get transfer reasons
- [x] Webhooks (v2)
- [x] Create a new webhook
- [x] Retrieve a list of webhooks
- [x] Retrieve a webhook
- [x] Update a webhook
- [x] Delete a webhook
- [x] Rotate a webhook signing secret
- [x] Retrieve a list of failed webhook events
- [x] Verify a webhook signature

## **Contributing**

In order to facilitate a streamlined development process, we have a few guidelines that we would like to follow. Please refer to the [CONTRIBUTING.md](https://github.com/Trevypants/pyrevolut/blob/main/CONTRIBUTING.md) file for more information.

## **License**

This project is licensed under the MIT License - see the [LICENSE](https://github.com/Trevypants/pyrevolut/blob/main/LICENSE) file for details.

**Disclaimer:** `pyrevolut` is an un-official API wrapper. It is in no way endorsed by or affiliated with Revolut or any associated organization. Make sure to read and understand the terms of service of the underlying API before using this package. The authors accept no responsiblity for any damage that might stem from use of this package.
65 changes: 45 additions & 20 deletions pyrevolut/client/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Type, TypeVar, Literal, Annotated
from typing import Type, TypeVar, Literal, Annotated, Callable
import logging
import json
import base64
Expand All @@ -19,8 +19,8 @@
from pyrevolut.utils.auth import (
ModelCreds,
refresh_access_token,
save_creds,
load_creds,
save_creds as save_creds_fn,
load_creds as load_creds_fn,
)
from pyrevolut.exceptions import (
PyRevolutBaseException,
Expand Down Expand Up @@ -60,6 +60,8 @@ class BaseClient:
sandbox: bool
return_type: Literal["raw", "dict", "model"] = "dict"
error_response: Literal["raw", "raise", "dict", "model"] = "raise"
custom_save_fn: Callable[[ModelCreds], None] | None = None
custom_load_fn: Callable[..., ModelCreds] | None = None
client: SyncClient | AsyncClient | None = None

def __init__(
Expand All @@ -69,6 +71,8 @@ def __init__(
sandbox: bool = True,
return_type: Literal["raw", "dict", "model"] = "dict",
error_response: Literal["raw", "raise", "dict", "model"] = "raise",
custom_save_fn: Callable[[ModelCreds], None] | None = None,
custom_load_fn: Callable[..., ModelCreds] | None = None,
):
"""Create a new Revolut client
Expand All @@ -80,7 +84,7 @@ def __init__(
creds : str | dict, optional
The credentials to use for the client, by default None. If not provided, will
load the credentials from the creds_loc file.
Can be a dictionary of the credentials or a base64 encoded string of the credentials dictionary.
Can be a dictionary of the credentials or a base64 encoded string of the credentials json.
sandbox : bool, optional
Whether to use the sandbox environment, by default True
return_type : Literal["raw", "dict", "model"], optional
Expand All @@ -102,6 +106,10 @@ def __init__(
The client will return a dictionary representation of the error response
If "model":
The client will return a Pydantic model of the error response
custom_save_fn : Callable[[ModelCreds], None], optional
A custom function to save the credentials, by default None
custom_load_fn : Callable[..., ModelCreds], optional
A custom function to load the credentials, by default None
"""
assert return_type in [
"raw",
Expand All @@ -113,12 +121,15 @@ def __init__(
"dict",
"model",
], "error_response must be 'raise', 'dict', or 'model'"
assert ".json" in creds_loc, "creds_loc must be a .json file"

self.creds_loc = creds_loc
self.creds = creds
self.sandbox = sandbox
self.return_type = return_type
self.error_response = error_response
self.custom_save_fn = custom_save_fn
self.custom_load_fn = custom_load_fn

# Set domain based on environment
if self.sandbox:
Expand Down Expand Up @@ -625,17 +636,19 @@ def __replace_null_with_none(self, data: D) -> D:
def load_credentials(self):
"""Load the credentials from the credentials inputs.
- If credentials are not provided, will load them from the credentials file.
- If the credentials file does not exist, raise an error.
- If the credentials file is invalid, raise an error.
- If the credentials are expired, raise an error.
- If the access token is expired, refresh it.
- If credentials are provided:
- If the credentials are a string, decode it and load the credentials.
- If the credentials are a dictionary, load the credentials.
- If credentials are not provided:
- If the custom load function is provided, use it.
- Otherwise load the credentials from the credentials file using the default loader / location. Expects a .json file.
"""
solution_msg = (
"\n\nPlease reauthenticate using the `pyrevolut auth-manual` command."
)

# Load the credentials
if self.creds is not None:
if isinstance(self.creds, str):
_creds = json.loads(base64.b64decode(self.creds).decode("utf-8"))
Expand All @@ -648,16 +661,19 @@ def load_credentials(self):
f"Error loading credentials: {exc}. {solution_msg}"
) from exc
else:
try:
self.credentials = load_creds(location=self.creds_loc)
except FileNotFoundError as exc:
raise ValueError(
f"Credentials file not found: {exc}. {solution_msg}"
) from exc
except Exception as exc:
raise ValueError(
f"Error loading credentials: {exc}. {solution_msg}"
) from exc
if self.custom_load_fn is not None:
self.credentials = self.custom_load_fn()
else:
try:
self.credentials = load_creds_fn(location=self.creds_loc)
except FileNotFoundError as exc:
raise ValueError(
f"Credentials file not found: {exc}. {solution_msg}"
) from exc
except Exception as exc:
raise ValueError(
f"Error loading credentials: {exc}. {solution_msg}"
) from exc

# Check if the credentials are still valid
if self.credentials.credentials_expired:
Expand All @@ -667,6 +683,13 @@ def load_credentials(self):
if self.credentials.access_token_expired:
self.refresh_access_token()

def save_credentials(self):
"""Save the credentials to the credentials file."""
if self.custom_save_fn is not None:
self.custom_save_fn(self.credentials)
else:
save_creds_fn(creds=self.credentials, location=self.creds_loc, indent=4)

def refresh_access_token(self):
"""Refresh the access token using the refresh token.
Will call the endpoint to refresh the access token.
Expand Down Expand Up @@ -697,6 +720,8 @@ def refresh_access_token(self):
self.credentials.tokens.access_token_expiration_dt = pendulum.now(
tz="UTC"
).add(seconds=resp.expires_in)
save_creds(creds=self.credentials, location=self.creds_loc, indent=4)

# Save the new credentials
self.save_credentials()
except Exception as exc:
raise ValueError(f"Error refreshing access token: {exc}.") from exc
Loading

0 comments on commit ae845f0

Please sign in to comment.