Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions numerapi/base_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,58 @@ def models_of_account(self, account) -> Dict[str, str]:
for item in sorted(data, key=lambda x: x["displayName"])
}

def public_user_profile(self, username: str) -> Dict:
"""Fetch the public profile of a user / model.

The model is resolved within this API's tournament
(``self.tournament_id``), so the returned ``id`` is the model UUID
for *this* tournament. That id is what e.g. :meth:`submission_scores`
expects as ``model_id``. This works identically for Numerai Classic,
Signals and Crypto - the only difference is the tournament the
concrete API class is configured for.

Args:
username (str): the model name (called "username" on the
Crypto / Signals leaderboards)

Returns:
dict: user profile including the following fields:

* username (`str`)
* startDate (`datetime`)
* id (`str`) - the model UUID, usable as `model_id`
* bio (`str`)
* nmrStaked (`decimal.Decimal`)

Example:
>>> api = CryptoAPI()
>>> api.public_user_profile("quixotic15")
{'bio': None,
'id': '0c58da1a-8df4-4e98-a99a-71a12fbbe36b',
'startDate': datetime.datetime(2024, 11, 28, ...),
'nmrStaked': None,
'username': 'quixotic15'}
"""
query = """
query($model_name: String!
$tournament: Int) {
v3UserProfile(model_name: $model_name
tournament: $tournament) {
id
startDate
username
bio
nmrStaked
}
}
"""
arguments = {"model_name": username, "tournament": self.tournament_id}
data = self.raw_query(query, arguments)["data"]["v3UserProfile"]
# convert strings to python objects
utils.replace(data, "startDate", utils.parse_datetime_string)
utils.replace(data, "nmrStaked", utils.parse_float_string)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve Classic nmrStaked return type

For NumerAPI.public_user_profile callers, this new shared conversion changes nmrStaked from the raw GraphQL string returned by the previous Classic implementation into a Decimal. That is a public API/serialization change unrelated to adding Crypto support; for example existing Classic users doing json.dumps(api.public_user_profile(...)) or string-based persistence/comparisons can now fail or behave differently. Keep the conversion scoped to APIs that already returned Decimal, or make the Classic behavior an explicit compatibility break.

Useful? React with 👍 / 👎.

return data

def get_models(self, tournament: int | None = None) -> Dict:
"""Get mapping of account model names to model ids for convenience

Expand Down
42 changes: 0 additions & 42 deletions numerapi/numerapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,48 +273,6 @@ def stake_get(self, modelname: str) -> float:
data = self.raw_query(query, arguments)['data']['v3UserProfile']
return data['stakeValue']

def public_user_profile(self, username: str) -> Dict:
"""Fetch the public profile of a user.

Args:
username (str)

Returns:
dict: user profile including the following fields:
* username (`str`)
* startDate (`datetime`)
* id (`string`)
* bio (`str`)
* nmrStaked (`float`)

Example:
>>> api = NumerAPI()
>>> api.public_user_profile("integration_test")
{'bio': 'The official example model. Submits example predictions.',
'id': '59de8728-38e5-45bd-a3d5-9d4ad649dd3f',
'startDate': datetime.datetime(
2018, 6, 6, 17, 33, 21, tzinfo=tzutc()),
'nmrStaked': '57.582371875005243780',
'username': 'integration_test'}

"""
query = """
query($model_name: String!) {
v3UserProfile(model_name: $model_name) {
id
startDate
username
bio
nmrStaked
}
}
"""
arguments = {'model_name': username}
data = self.raw_query(query, arguments)['data']['v3UserProfile']
# convert strings to python objects
utils.replace(data, "startDate", utils.parse_datetime_string)
return data

def daily_model_performances(self, username: str) -> List[Dict]:
"""Fetch daily performance of a user.

Expand Down
43 changes: 0 additions & 43 deletions numerapi/signalsapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,49 +151,6 @@ def upload_predictions(self, file_path: str = "predictions.csv",
create = self.raw_query(create_query, arguments, authorization=True)
return create['data']['createSignalsSubmission']['id']

def public_user_profile(self, username: str) -> Dict:
"""Fetch the public Numerai Signals profile of a user.

Args:
username (str)

Returns:
dict: user profile including the following fields:

* username (`str`)
* startDate (`datetime`)
* id (`string`)
* bio (`str`)
* nmrStaked (`decimal.Decimal`)

Example:
>>> api = SignalsAPI()
>>> api.public_user_profile("floury_kerril_moodle")
{'bio': None,
'id': '635db2a4-bdc6-4e5d-b515-f5120392c8c9',
'startDate': datetime.datetime(2019, 3, 26, 0, 43),
'username': 'floury_kerril_moodle',
'nmrStaked': Decimal('14.630994874320760131')}

"""
query = """
query($username: String!) {
v2SignalsProfile(modelName: $username) {
id
startDate
username
bio
nmrStaked
}
}
"""
arguments = {'username': username}
data = self.raw_query(query, arguments)['data']['v2SignalsProfile']
# convert strings to python objects
utils.replace(data, "startDate", utils.parse_datetime_string)
utils.replace(data, "nmrStaked", utils.parse_float_string)
return data

def daily_model_performances(self, username: str) -> List[Dict]:
"""Fetch daily Numerai Signals performance of a model.

Expand Down
41 changes: 41 additions & 0 deletions tests/test_cryptoapi.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import decimal
from unittest.mock import patch

Expand Down Expand Up @@ -41,3 +42,43 @@ def test_get_leaderboard(mocked, api):
assert "cryptosignalsLeaderboard" in args[0]
assert args[1] == {"limit": 1, "offset": 0}
assert kwargs == {}


@patch("numerapi.cryptoapi.CryptoAPI.raw_query")
def test_public_user_profile(mocked, api):
mocked.return_value = {
"data": {
"v3UserProfile": {
"id": "08d44800-be35-41f5-9896-63a1be9c51ef",
"username": "crypto_user",
"startDate": "2024-11-28T13:09:20Z",
"bio": None,
"nmrStaked": "13.0",
}
}
}

profile = api.public_user_profile("crypto_user")

assert profile["id"] == "08d44800-be35-41f5-9896-63a1be9c51ef"
assert profile["username"] == "crypto_user"
# string fields are converted to python objects
assert isinstance(profile["startDate"], datetime.datetime)
assert profile["nmrStaked"] == decimal.Decimal("13.0")
mocked.assert_called_once()
args, kwargs = mocked.call_args
# crypto must be resolved by name *within its own tournament* (12),
# otherwise a model id from another tournament could be returned
assert "v3UserProfile" in args[0]
assert args[1]["tournament"] == api.tournament_id == 12


@patch("numerapi.cryptoapi.CryptoAPI.raw_query")
def test_public_user_profile_to_model_id(mocked, api):
# the model id is what `submission_scores` needs
mocked.return_value = {
"data": {"v3UserProfile": {
"id": "the-model-id", "username": "crypto_user",
"startDate": None, "bio": None, "nmrStaked": None}}}
model_id = api.public_user_profile("crypto_user")["id"]
assert model_id == "the-model-id"
19 changes: 19 additions & 0 deletions tests/test_signalsapi.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from unittest.mock import patch

import pytest
import responses

Expand All @@ -7,6 +9,23 @@
from numerapi import base_api


@patch("numerapi.base_api.Api.raw_query")
def test_public_user_profile(mocked, api):
mocked.return_value = {
"data": {"v3UserProfile": {
"id": "49962e16-6bc9-4a78-a751-09c20c99bcb3",
"username": "floury_kerril_moodle",
"startDate": "2020-05-12T01:23:00Z",
"bio": None, "nmrStaked": None}}}

profile = api.public_user_profile("floury_kerril_moodle")

assert profile["id"] == "49962e16-6bc9-4a78-a751-09c20c99bcb3"
args, _ = mocked.call_args
# signals models must be resolved within the signals tournament (11)
assert args[1]["tournament"] == api.tournament_id == 11


@pytest.fixture(scope='function', name="api")
def api_fixture():
api = numerapi.SignalsAPI(verbosity='DEBUG')
Expand Down