Compare commits

...

14 Commits

Author SHA1 Message Date
b8ae633ed5 Update readme 2023-05-12 20:53:44 +02:00
ea690f0479 Remove deprecated product functions 2023-05-11 19:54:23 +02:00
fc79f5c579 Remove purchase product code and add details to exceptions 2023-05-11 17:05:55 +02:00
ed8f93d7d0 Set required python version to 3.7 2023-04-23 11:51:06 +02:00
dfd582a439 Set required python version to 3.7 2023-04-23 11:48:11 +02:00
96adaac01a Update readme 2023-04-23 10:06:25 +02:00
fb6d1276e6 Better error handling 2023-04-23 10:03:50 +02:00
6a64709d17 Product methods for python SDK 2023-04-21 23:39:20 +02:00
270f742c95 Remove port parameter 2023-04-21 17:56:35 +02:00
fd71fa276c Product purchase functions 2023-04-10 20:02:23 +02:00
edca936325 uuid -> id 2023-03-31 17:19:26 +02:00
10290dba54 Update main readme 2023-03-27 14:20:34 +02:00
11fb366423 Update readme files 2023-03-27 14:16:52 +02:00
818617ddd4 Python SDK readme 2023-03-27 14:09:36 +02:00
10 changed files with 191 additions and 58 deletions

4
README.md Normal file
View File

@ -0,0 +1,4 @@
# FairHopper SDK for Python and JavaScript
- [Python](python/README.md)
- [JavaScript](javascript/README.md)

20
javascript/README.md Normal file
View File

@ -0,0 +1,20 @@
# FairHopper JavaScript SDK
Requirements:
- Node 18+
## Running demo
Check `demo.js` for usage example.
Edit `demo.js` and configure FairHopper Game host settings:
```javascript
const FAIRHOPPER_HOST = "http://127.0.0.1:8010";
```
Run example using node:
```sh
node demo.js
```

View File

@ -1,12 +1,11 @@
const sdk = require("./fh_sdk"); const sdk = require("./fh_sdk");
const FAIRHOPPER_HOST = "http://127.0.0.1"; const FAIRHOPPER_HOST = "https://api.fairhopper.mjerenja.com";
const FAIRHOPPER_PORT = 8010;
async function main() { async function main() {
const fh = new sdk.FairHopper(FAIRHOPPER_HOST, FAIRHOPPER_PORT); const fh = new sdk.FairHopper(FAIRHOPPER_HOST);
console.log(`Pinging FairHopper server on ${FAIRHOPPER_HOST}:${FAIRHOPPER_PORT}`); console.log(`Pinging FairHopper server on ${FAIRHOPPER_HOST}`);
const pingResult = await fh.ping(); const pingResult = await fh.ping();
console.log("Ping result:", pingResult); console.log("Ping result:", pingResult);
console.log(); console.log();
@ -18,23 +17,23 @@ async function main() {
console.log("Initial player position is", game.player.position); console.log("Initial player position is", game.player.position);
let moveResult; let moveResult;
const playerUuid = game.player.uuid; const playerId = game.player.id;
try { try {
console.log("Trying to move right"); console.log("Trying to move right");
moveResult = await fh.move(playerUuid, sdk.Direction.RIGHT); moveResult = await fh.move(playerId, sdk.Direction.RIGHT);
console.log("Successfully moved to", moveResult.player.position); console.log("Successfully moved to", moveResult.player.position);
console.log("Trying to move right"); console.log("Trying to move right");
moveResult = await fh.move(playerUuid, sdk.Direction.RIGHT); moveResult = await fh.move(playerId, sdk.Direction.RIGHT);
console.log("Successfully moved to", moveResult.player.position); console.log("Successfully moved to", moveResult.player.position);
console.log("Trying to move right"); console.log("Trying to move right");
moveResult = await fh.move(playerUuid, sdk.Direction.RIGHT); moveResult = await fh.move(playerId, sdk.Direction.RIGHT);
console.log("Successfully moved to", moveResult.player.position); console.log("Successfully moved to", moveResult.player.position);
console.log("Trying to move down"); console.log("Trying to move down");
moveResult = await fh.move(playerUuid, sdk.Direction.DOWN); moveResult = await fh.move(playerId, sdk.Direction.DOWN);
console.log("Successfully moved to", moveResult.player.position); console.log("Successfully moved to", moveResult.player.position);
} catch (err) { } catch (err) {
if (err instanceof sdk.PositionError) { if (err instanceof sdk.PositionError) {

View File

@ -1,3 +1,13 @@
class ValidationError extends Error {
constructor(message) {
if (!message) {
message = "ValidationError";
}
super(message);
this.name = "ValidationError";
}
}
class PlayerNotFoundError extends Error { class PlayerNotFoundError extends Error {
constructor(message) { constructor(message) {
if (!message) { if (!message) {
@ -36,9 +46,8 @@ const Direction = {
}; };
class FairHopper { class FairHopper {
constructor(host, port) { constructor(host) {
this.host = host; this.host = host;
this.port = port;
this.defaultHeaders = { this.defaultHeaders = {
Accept: "application/json", Accept: "application/json",
@ -47,7 +56,7 @@ class FairHopper {
} }
formatUrl(path) { formatUrl(path) {
return `${this.host}:${this.port}${path}`; return `${this.host}${path}`;
} }
async ping() { async ping() {
@ -66,6 +75,10 @@ class FairHopper {
headers: this.defaultHeaders, headers: this.defaultHeaders,
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
switch (r.status) {
case 422:
throw new ValidationError();
}
return await r.json(); return await r.json();
} }
@ -76,8 +89,8 @@ class FairHopper {
return await r.json(); return await r.json();
} }
async getPlayerInfo(playerUuid) { async getPlayerInfo(playerId) {
const r = await fetch(this.formatUrl(`/player/${playerUuid}`), { const r = await fetch(this.formatUrl(`/player/${playerId}`), {
headers: this.defaultHeaders, headers: this.defaultHeaders,
}); });
switch (r.status) { switch (r.status) {
@ -85,28 +98,30 @@ class FairHopper {
throw new PlayerInactiveError(); throw new PlayerInactiveError();
case 404: case 404:
throw new PlayerNotFoundError(); throw new PlayerNotFoundError();
case 422:
throw new ValidationError();
} }
return await r.json(); return await r.json();
} }
async moveLeft(playerUuid) { async moveLeft(playerId) {
return await this.move(playerUuid, "left"); return await this.move(playerId, "left");
} }
async moveRight(playerUuid) { async moveRight(playerId) {
return await this.move(playerUuid, "right"); return await this.move(playerId, "right");
} }
async moveUp(playerUuid) { async moveUp(playerId) {
return await this.move(playerUuid, "up"); return await this.move(playerId, "up");
} }
async moveDown(playerUuid) { async moveDown(playerId) {
return await this.move(playerUuid, "down"); return await this.move(playerId, "down");
} }
async move(playerUuid, direction) { async move(playerId, direction) {
const url = this.formatUrl(`/player/${playerUuid}/move/${direction}`); const url = this.formatUrl(`/player/${playerId}/move/${direction}`);
const r = await fetch(url, { const r = await fetch(url, {
method: "post", method: "post",
headers: this.defaultHeaders, headers: this.defaultHeaders,
@ -118,6 +133,8 @@ class FairHopper {
throw new PlayerNotFoundError(); throw new PlayerNotFoundError();
case 409: case 409:
throw new PositionError(); throw new PositionError();
case 422:
throw new ValidationError();
} }
return await r.json(); return await r.json();
} }

62
python/README.md Normal file
View File

@ -0,0 +1,62 @@
# FairHopper Python SDK
Requirements:
- Python 3.7+
## Setting up environment
### Recommended: Using Poetry
Project uses [Poetry](https://python-poetry.org), ultimate dependency management software for Python.
Install Poetry:
```sh
pip install poetry
```
Install virtual environment:
```sh
poetry install
```
### Simple: Using virtualenv
Create virtual environment:
```sh
virtualenv .env
```
Activate virtual environment:
```sh
source .env/bin/activate
```
Install required dependencies:
```sh
pip install -r requirements.txt
```
## Running demo
Check `demo.py` for usage example.
Edit `demo.py` and configure FairHopper Game host settings:
```python
FAIRHOPPER_HOST = "http://127.0.0.1:8010"
```
Activate environment and run example:
### Poetry
```sh
poetry shell
python demo.py
```
### Virtualenv
```sh
source .env/bin/activate
python demo.py
```

View File

@ -1,13 +1,12 @@
from fh_sdk import Direction, FairHopper, PositionError from fh_sdk import Direction, FairHopper, PositionError
FAIRHOPPER_HOST = "http://127.0.0.1" FAIRHOPPER_HOST = "https://api.fairhopper.mjerenja.com"
FAIRHOPPER_PORT = 8010
def main() -> None: def main() -> None:
fh = FairHopper(FAIRHOPPER_HOST, FAIRHOPPER_PORT) fh = FairHopper(FAIRHOPPER_HOST)
print(f"Pinging FairHopper server on {FAIRHOPPER_HOST}:{FAIRHOPPER_PORT}") print(f"Pinging FairHopper server on {FAIRHOPPER_HOST}")
res = fh.ping() res = fh.ping()
print("Ping result:", res) print("Ping result:", res)
print() print()
@ -18,23 +17,23 @@ def main() -> None:
print() print()
print("Initial player position is", game.player.position) print("Initial player position is", game.player.position)
player_uuid = game.player.uuid player_id = game.player.id
try: try:
print("Trying to move right") print("Trying to move right")
moveResult = fh.move(player_uuid, Direction.RIGHT) moveResult = fh.move(player_id, Direction.RIGHT)
print("Successfully moved to", moveResult.player.position) print("Successfully moved to", moveResult.player.position)
print("Trying to move right") print("Trying to move right")
moveResult = fh.move(player_uuid, Direction.RIGHT) moveResult = fh.move(player_id, Direction.RIGHT)
print("Successfully moved to", moveResult.player.position) print("Successfully moved to", moveResult.player.position)
print("Trying to move right") print("Trying to move right")
moveResult = fh.move(player_uuid, Direction.RIGHT) moveResult = fh.move(player_id, Direction.RIGHT)
print("Successfully moved to", moveResult.player.position) print("Successfully moved to", moveResult.player.position)
print("Trying to move down") print("Trying to move down")
moveResult = fh.move(player_uuid, Direction.DOWN) moveResult = fh.move(player_id, Direction.DOWN)
print("Successfully moved to", moveResult.player.position) print("Successfully moved to", moveResult.player.position)
except PositionError: except PositionError:
print("Cant't move in this direction") print("Cant't move in this direction")

View File

@ -1,23 +1,35 @@
from pydantic import BaseModel
from enum import Enum from enum import Enum
from typing import List, Optional
import requests import requests
from pydantic import BaseModel
class BaseError(Exception): class BaseError(Exception):
... detail: str = "BaseError"
def __str__(self) -> str:
return self.detail
class PositionError(BaseError): class PositionError(BaseError):
... detail = "Invalid position"
class PlayerInactiveError(BaseError): class PlayerInactiveError(BaseError):
... detail = "Player inactive"
class PlayerNotFoundError(BaseError): class PlayerNotFoundError(BaseError):
... detail = "Player not found"
class GameLockedError(BaseError):
detail = "Game locked. Product selection in progress."
class ValidationError(BaseError):
detail = "Validation error"
class Direction(str, Enum): class Direction(str, Enum):
@ -27,6 +39,13 @@ class Direction(str, Enum):
DOWN = "down" DOWN = "down"
class PlayerState(str, Enum):
CREATED = "CREATED"
MOVING = "MOVING"
ON_DESTINATION = "ON_DESTINATION"
INACTIVE = "INACTIVE"
class Board(BaseModel): class Board(BaseModel):
width: int width: int
height: int height: int
@ -42,12 +61,13 @@ class Destination(BaseModel):
class Player(BaseModel): class Player(BaseModel):
uuid: str id: str
name: str name: str
active: bool active: bool
position: Position position: Position
move_count: int move_count: int
move_attempt_count: int move_attempt_count: int
state: PlayerState
class PingResponse(BaseModel): class PingResponse(BaseModel):
@ -70,12 +90,11 @@ class GameInfoResponse(BaseModel):
class FairHopper: class FairHopper:
def __init__(self, host, port) -> None: def __init__(self, host: str) -> None:
self.host = host self.host = host
self.port = port
def format_url(self, path: str) -> str: def format_url(self, path: str) -> str:
return f"{self.host}:{self.port}{path}" return f"{self.host}{path}"
def ping(self) -> PingResponse: def ping(self) -> PingResponse:
r = requests.get(self.format_url("/ping")) r = requests.get(self.format_url("/ping"))
@ -87,7 +106,12 @@ class FairHopper:
"player_name": player_name, "player_name": player_name,
} }
r = requests.post(self.format_url("/game"), json=payload) r = requests.post(self.format_url("/game"), json=payload)
r.raise_for_status()
if r.status_code == 422:
raise ValidationError()
else:
r.raise_for_status()
return StartGameResponse(**r.json()) return StartGameResponse(**r.json())
def get_game_info(self) -> GameInfoResponse: def get_game_info(self) -> GameInfoResponse:
@ -95,32 +119,34 @@ class FairHopper:
r.raise_for_status() r.raise_for_status()
return GameInfoResponse(**r.json()) return GameInfoResponse(**r.json())
def get_player_info(self, uuid: str) -> PlayerInfoResponse: def get_player_info(self, player_id: str) -> PlayerInfoResponse:
r = requests.get(self.format_url(f"/player/{uuid}")) r = requests.get(self.format_url(f"/player/{player_id}"))
if r.status_code == 403: if r.status_code == 403:
raise PlayerInactiveError() raise PlayerInactiveError()
elif r.status_code == 404: elif r.status_code == 404:
raise PlayerNotFoundError() raise PlayerNotFoundError()
elif r.status_code == 422:
raise ValidationError()
else: else:
r.raise_for_status() r.raise_for_status()
return PlayerInfoResponse(**r.json()) return PlayerInfoResponse(**r.json())
def move_left(self, uuid: str) -> PlayerInfoResponse: def move_left(self, player_id: str) -> PlayerInfoResponse:
return self.move(uuid, Direction.LEFT) return self.move(player_id, Direction.LEFT)
def move_right(self, uuid: str) -> PlayerInfoResponse: def move_right(self, player_id: str) -> PlayerInfoResponse:
return self.move(uuid, Direction.RIGHT) return self.move(player_id, Direction.RIGHT)
def move_up(self, uuid: str) -> PlayerInfoResponse: def move_up(self, player_id: str) -> PlayerInfoResponse:
return self.move(uuid, Direction.UP) return self.move(player_id, Direction.UP)
def move_down(self, uuid: str) -> PlayerInfoResponse: def move_down(self, player_id: str) -> PlayerInfoResponse:
return self.move(uuid, Direction.DOWN) return self.move(player_id, Direction.DOWN)
def move(self, uuid: str, direction: Direction) -> PlayerInfoResponse: def move(self, player_id: str, direction: Direction) -> PlayerInfoResponse:
path = f"/player/{uuid}/move/{direction}" path = f"/player/{player_id}/move/{direction}"
r = requests.post(self.format_url(path)) r = requests.post(self.format_url(path))
if r.status_code == 403: if r.status_code == 403:
@ -129,6 +155,10 @@ class FairHopper:
raise PlayerNotFoundError() raise PlayerNotFoundError()
elif r.status_code == 409: elif r.status_code == 409:
raise PositionError() raise PositionError()
elif r.status_code == 422:
raise ValidationError()
elif r.status_code == 423:
raise GameLockedError()
else: else:
r.raise_for_status() r.raise_for_status()

4
python/poetry.lock generated
View File

@ -215,5 +215,5 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" python-versions = "^3.7"
content-hash = "a8d95adf4819c22b47d4cac44f18a3bc5040d1c0487ead04cb2f09692de0d411" content-hash = "6d7fc4c2ae40fa75f772d15817348dd97078b9aa25ecd3ad52e71bec25732be8"

View File

@ -7,7 +7,7 @@ readme = "README.md"
packages = [{include = "fh_sdk"}] packages = [{include = "fh_sdk"}]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10" python = "^3.7"
requests = "^2.28.2" requests = "^2.28.2"
pydantic = "^1.10.7" pydantic = "^1.10.7"

2
python/requirements.txt Normal file
View File

@ -0,0 +1,2 @@
pydantic
requests