py wrapper

This commit is contained in:
nise.moe 2024-06-12 14:17:19 +02:00
parent 64f3960a1d
commit e279c4a63f
8 changed files with 519 additions and 0 deletions

View File

@ -0,0 +1,20 @@
<div class="main term mb-2">
<div class="fade-stuff">
<button [routerLink]="['/docs']">< back to api docs</button>
<h1 class="mb-4"># super duper official python library</h1>
<p>if you're using python, you can download/copy the api wrapper into your project and get (more or less) type-safe methods.</p>
<p>last update: <code>v20240612</code></p>
<p><u>remember</u> to <code>pip install requests</code></p>
<div class="text-center">
<button (click)="copyToClipboard()">copy to clipboard</button>
<button (click)="download()" style="margin-left: 8px">download .py</button>
</div>
<hr class="mt-4 mb-4">
<h1>Example usage</h1>
<pre style="font-family: monospace, monospace !important">{{ pythonUsage }}</pre>
</div>
</div>

View File

@ -0,0 +1,59 @@
import { Component } from '@angular/core';
import {Observable} from "rxjs";
import {HttpClient} from "@angular/common/http";
import {RouterLink} from "@angular/router";
@Component({
selector: 'app-api-python',
standalone: true,
imports: [
RouterLink
],
templateUrl: './api-python.component.html',
styleUrl: './api-python.component.css'
})
export class ApiPythonComponent {
pythonScript!: string;
pythonUsage!: string;
ngOnInit(): void {
this.getPythonScript().subscribe(
data => this.pythonScript = data,
error => console.error(error)
);
this.getPythonScriptUsage().subscribe(
data => this.pythonUsage = data,
error => console.error(error)
);
}
constructor(private http: HttpClient) { }
getPythonScript(): Observable<string> {
return this.http.get('/assets/nise.py', { responseType: 'text' });
}
getPythonScriptUsage(): Observable<string> {
return this.http.get('/assets/nise_usage.py', { responseType: 'text' });
}
copyToClipboard(): void {
navigator.clipboard.writeText(this.pythonScript)
.then(() => alert('Copied to clipboard!'))
.catch(() => alert('Failed to copy to clipboard!'));
}
download(): void {
const blob = new Blob([this.pythonScript], {type: 'text/plain'});
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'nise.py';
a.click();
window.URL.revokeObjectURL(url);
}
}

View File

@ -2,3 +2,11 @@ section {
padding-bottom: 30px;
border-bottom: 2px dotted rgba(224, 224, 224, 0.32);
}
.cursor-pointer {
cursor: pointer;
}
.py-wrapper:hover {
background-color: rgba(139, 139, 139, 0.32);
}

View File

@ -1,9 +1,17 @@
<div class="main term mb-2">
<div class="fade-stuff">
<h1 class="mb-4"># <span class="board">/api/</span> stuff</h1>
<div class="alert mb-2 text-center cursor-pointer py-wrapper" [routerLink]="['/docs/py']">
<p>
<span style="padding: 6px; background-color: #2af171; color: #000000">NEW!</span> check out the python api
wrapper
</p>
</div>
<p>
if you'd like to retrieve data from our database, you are invited to use the same endpoints meant for the frontend but in a programmatic way. <u>currently there's no rate limits.</u>
</p>
<div class="alert mb-2 text-center">
<p>
if you have any issues related to CloudFlare, let me know on discord. currently all api requests are routed via CF so it's possible it might block certain IPs.

View File

@ -11,6 +11,7 @@ import {ContributeComponent} from "./contribute/contribute.component";
import {BanlistComponent} from "./banlist/banlist.component";
import {ProfileComponent} from "./profile/profile.component";
import {ApiComponent} from "./api/api.component";
import {ApiPythonComponent} from "./api-python/api-python.component";
const routes: Routes = [
{path: 'sus/:f', component: ViewSuspiciousScoresComponent, title: '/sus/'},
@ -33,6 +34,7 @@ const routes: Routes = [
{path: 'contribute', component: ContributeComponent, title: '/contribute/ <3'},
{path: 'docs', component: ApiComponent, title: '/api/ explanation'},
{path: 'docs/py', component: ApiPythonComponent, title: 'python lib'},
{path: '**', component: HomeComponent, title: '/nise.moe/'},
];

View File

@ -0,0 +1,328 @@
import datetime
import requests
import json
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Any
@dataclass
class SearchPagination:
currentPage: int
pageSize: int
totalPages: int
totalResults: int
@dataclass
class ScoreSearchResult:
user_id: int
user_username: str
user_join_date: str
user_country: str
user_rank: int
user_pp_raw: float
user_accuracy: float
user_playcount: int
user_total_score: int
user_ranked_score: int
user_seconds_played: int
user_count_300: int
user_count_100: int
user_count_50: int
user_count_miss: int
user_is_banned: bool
id: int
beatmap_id: int
count_300: int
count_100: int
count_50: int
count_miss: int
date: str
max_combo: int
mods: int
perfect: bool
pp: float
rank: str
replay_id: int
score: int
ur: float
frametime: float
edge_hits: int
snaps: int
adjusted_ur: float
mean_error: float
error_variance: float
error_standard_deviation: float
minimum_error: int
maximum_error: int
error_range: int
error_coefficient_of_variation: float
error_kurtosis: float
error_skewness: float
keypresses_median_adjusted: float
keypresses_standard_deviation_adjusted: float
sliderend_release_median_adjusted: float
sliderend_release_standard_deviation_adjusted: float
beatmap_artist: str
beatmap_beatmapset_id: int
beatmap_creator: str
beatmap_source: Optional[str]
beatmap_star_rating: float
beatmap_title: str
beatmap_version: str
@dataclass
class ScoreSearchResult:
pagination: SearchPagination
results: List[ScoreSearchResult]
@dataclass
class UserSearchResult:
user_id: int
user_username: str
user_join_date: str
user_country: str
user_rank: int
user_pp_raw: float
user_accuracy: float
user_playcount: int
user_total_score: int
user_ranked_score: int
user_seconds_played: int
user_count_300: int
user_count_100: int
user_count_50: int
user_count_miss: int
user_is_banned: bool
@dataclass
class UserSearchResult:
pagination: SearchPagination
results: List[UserSearchResult]
@dataclass
class ErrorDistribution:
countMiss: float
count300: float
count100: float
count50: float
@dataclass
class ReplayData:
replay_id: int
user_id: int
username: str
date: str
beatmap_id: int
beatmap_beatmapset_id: int
beatmap_artist: str
beatmap_title: str
beatmap_star_rating: float
beatmap_creator: str
beatmap_version: str
beatmap_max_combo: int
beatmap_total_length: int
beatmap_bpm: float
beatmap_accuracy: float
beatmap_ar: float
beatmap_cs: float
beatmap_drain: float
beatmap_count_circles: int
beatmap_count_sliders: int
beatmap_count_spinners: int
score: int
mods: List[str]
rank: str
ur: float
adjusted_ur: float
frametime: int
snaps: int
hits: int
mean_error: float
error_variance: float
error_standard_deviation: float
minimum_error: float
maximum_error: float
error_range: float
error_coefficient_of_variation: float
error_kurtosis: float
error_skewness: float
comparable_samples: int
comparable_mean_error: float
comparable_error_variance: float
comparable_error_standard_deviation: float
comparable_minimum_error: float
comparable_maximum_error: float
comparable_error_range: float
comparable_error_coefficient_of_variation: float
comparable_error_kurtosis: float
comparable_error_skewness: float
comparable_adjusted_ur: float
pp: float
perfect: bool
max_combo: int
count_300: int
count_100: int
count_50: int
count_miss: int
error_distribution: Dict[str, ErrorDistribution]
similar_scores: List[Any]
charts: Any
@dataclass
class UserDetails:
user_id: int
username: str
rank: int
pp_raw: float
join_date: str
seconds_played: int
country: str
country_rank: Optional[int]
playcount: int
suspicious_scores: int
stolen_replays: int
@dataclass
class QueueDetails:
isProcessing: bool
lastCompletedUpdate: Optional[str]
canUpdate: bool
progressCurrent: Optional[int]
progressTotal: Optional[int]
@dataclass
class UserProfile:
user_details: UserDetails
queue_details: QueueDetails
suspicious_scores: List[dict] = field(default_factory=list)
similar_replays: List[dict] = field(default_factory=list)
total_scores: int = 0
is_banned: bool = False
approximate_ban_date: Optional[str] = None
@dataclass
class SuspiciousScoreEntry:
user_id: int
username: str
replay_id: int
date: str
beatmap_id: int
beatmap_beatmapset_id: int
beatmap_title: str
beatmap_star_rating: float
pp: float
frametime: float
ur: float
@dataclass
class StolenReplayEntry:
replay_id_1: int
replay_id_2: int
user_id_1: int
user_id_2: int
username_1: str
username_2: str
beatmap_beatmapset_id: int
replay_date_1: str
replay_date_2: str
replay_pp_1: float
replay_pp_2: float
beatmap_id: int
beatmap_title: str
beatmap_star_rating: float
similarity: float
@dataclass
class BanListPagination:
currentPage: int
pageSize: int
totalPages: int
totalResults: int
@dataclass
class BanListEntry:
userId: int
username: str
secondsPlayed: int
pp: float
rank: int
isBanned: bool
approximateBanTime: datetime
lastUpdate: datetime
@dataclass
class BanList:
pagination: BanListPagination
users: List[BanListEntry]
class NiseAPI:
def __init__(self, base_url="https://nise.moe"):
self.base_url = base_url
self.headers = {
"X-NISE-API": "20240218",
"Accept": "application/json",
"Content-Type": "application/json"
}
def search_scores(self, query) -> ScoreSearchResult:
url = f"{self.base_url}/api/search"
response = requests.post(url, headers=self.headers, data=json.dumps(query))
return ScoreSearchResult(**response.json())
def search_users(self, query) -> UserSearchResult:
url = f"{self.base_url}/api/search-user"
response = requests.post(url, headers=self.headers, data=json.dumps(query))
return UserSearchResult(**response.json())
def get_single_score(self, replay_id) -> ReplayData:
url = f"{self.base_url}/api/score/{replay_id}"
response = requests.get(url, headers=self.headers)
try:
return ReplayData(**response.json())
except (TypeError, ValueError):
raise ValueError()
def get_user_details(self, user_id=None, username=None) -> UserProfile:
url = f"{self.base_url}/api/user-details"
data = {}
if user_id:
data["userId"] = user_id
elif username:
data["username"] = username
response = requests.post(url, headers=self.headers, data=json.dumps(data))
try:
return UserProfile(**response.json())
except (TypeError, ValueError):
raise ValueError()
def get_suspicious_scores(self) -> List[SuspiciousScoreEntry]:
url = f"{self.base_url}/api/suspicious-scores"
response = requests.get(url, headers=self.headers)
return [SuspiciousScoreEntry(**item) for item in response.json()]
def get_similar_replays(self) -> List[StolenReplayEntry]:
url = f"{self.base_url}/api/similar-replays"
response = requests.get(url, headers=self.headers)
return [StolenReplayEntry(**item) for item in response.json()]
def get_banlist(self, page) -> BanList:
url = f"{self.base_url}/api/banlist"
data = {"page": page}
response = requests.post(url, headers=self.headers, data=json.dumps(data))
return BanList(**response.json())

View File

@ -0,0 +1,94 @@
nise_api = NiseAPI()
scores = nise_api.get_single_score(replay_id=3808640439)
print(scores.adjusted_ur)
user = nise_api.get_user_details(username="degenerate")
print(user)
suspicious_scores = nise_api.get_suspicious_scores()
print(suspicious_scores)
stolen_replays = nise_api.get_similar_replays()
print(stolen_replays)
banlist = nise_api.get_banlist(page=1)
print(banlist)
score_query = {
"queries": [
{
"predicates": [
{
"field": {
"name": "user_rank",
"type": "number"
},
"operator": {
"operatorType": "<",
"acceptsValues": "any"
},
"value": "50"
},
{
"field": {
"name": "ur",
"type": "number"
},
"operator": {
"operatorType": "<",
"acceptsValues": "any"
},
"value": "120"
}
],
"logicalOperator": "AND"
}
],
"sorting": {
"field": "user_id",
"order": "ASC"
},
"page": 1
}
score_search = nise_api.search_scores(query=score_query)
print(score_search)
user_query = {
"queries": [
{
"predicates": [
{
"field": {
"name": "rank",
"type": "number"
},
"operator": {
"operatorType": ">",
"acceptsValues": "any"
},
"value": "10"
},
{
"field": {
"name": "username",
"type": "string"
},
"operator": {
"operatorType": "=",
"acceptsValues": "any"
},
"value": "degenerate"
}
],
"logicalOperator": "AND"
}
],
"sorting": {
"field": "user_id",
"order": "ASC"
},
"page": 1
}
user_search = nise_api.search_users(query=user_query)
print(user_search)