fintables 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. fintables/__init__.py +8 -0
  2. fintables/api/__init__.py +3 -0
  3. fintables/api/auth.py +87 -0
  4. fintables/api/client.py +184 -0
  5. fintables/api/endpoints/__init__.py +65 -0
  6. fintables/api/endpoints/agenda.py +23 -0
  7. fintables/api/endpoints/analyst_ratings.py +26 -0
  8. fintables/api/endpoints/companies.py +9 -0
  9. fintables/api/endpoints/feed.py +47 -0
  10. fintables/api/endpoints/funds.py +14 -0
  11. fintables/api/endpoints/memo.py +36 -0
  12. fintables/api/endpoints/newsletter.py +36 -0
  13. fintables/api/endpoints/notifications.py +41 -0
  14. fintables/api/endpoints/portfolio.py +95 -0
  15. fintables/api/endpoints/post.py +32 -0
  16. fintables/api/endpoints/search.py +106 -0
  17. fintables/api/endpoints/sheets.py +12 -0
  18. fintables/api/endpoints/symbols.py +9 -0
  19. fintables/api/endpoints/video.py +34 -0
  20. fintables/api/endpoints/watchlist.py +22 -0
  21. fintables/cli/__init__.py +3 -0
  22. fintables/cli/commands/__init__.py +33 -0
  23. fintables/cli/commands/agenda.py +54 -0
  24. fintables/cli/commands/analyst.py +43 -0
  25. fintables/cli/commands/auth.py +47 -0
  26. fintables/cli/commands/company.py +31 -0
  27. fintables/cli/commands/feed.py +43 -0
  28. fintables/cli/commands/fund.py +38 -0
  29. fintables/cli/commands/memo.py +118 -0
  30. fintables/cli/commands/newsletter.py +83 -0
  31. fintables/cli/commands/notification.py +90 -0
  32. fintables/cli/commands/portfolio.py +288 -0
  33. fintables/cli/commands/post.py +70 -0
  34. fintables/cli/commands/search.py +31 -0
  35. fintables/cli/commands/symbol.py +57 -0
  36. fintables/cli/commands/video.py +83 -0
  37. fintables/cli/commands/watchlist.py +54 -0
  38. fintables/cli/main.py +112 -0
  39. fintables/config.py +53 -0
  40. fintables/exceptions.py +19 -0
  41. fintables/i18n.py +1199 -0
  42. fintables/models/__init__.py +129 -0
  43. fintables/models/agenda.py +20 -0
  44. fintables/models/analyst_rating.py +36 -0
  45. fintables/models/company.py +40 -0
  46. fintables/models/feed.py +143 -0
  47. fintables/models/fund.py +87 -0
  48. fintables/models/memo.py +21 -0
  49. fintables/models/newsletter.py +59 -0
  50. fintables/models/notification.py +25 -0
  51. fintables/models/portfolio.py +42 -0
  52. fintables/models/post.py +46 -0
  53. fintables/models/search.py +28 -0
  54. fintables/models/sheets.py +28 -0
  55. fintables/models/symbol.py +75 -0
  56. fintables/models/video.py +29 -0
  57. fintables/models/watchlist.py +12 -0
  58. fintables/output/__init__.py +31 -0
  59. fintables/output/formatter.py +954 -0
  60. fintables-0.1.0.dist-info/METADATA +194 -0
  61. fintables-0.1.0.dist-info/RECORD +65 -0
  62. fintables-0.1.0.dist-info/WHEEL +5 -0
  63. fintables-0.1.0.dist-info/entry_points.txt +2 -0
  64. fintables-0.1.0.dist-info/licenses/LICENSE +21 -0
  65. fintables-0.1.0.dist-info/top_level.txt +1 -0
fintables/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Fintables Python Client & CLI."""
2
+
3
+ from fintables.api.client import FintablesClient
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ __all__ = ["FintablesClient", "__version__"]
8
+
@@ -0,0 +1,3 @@
1
+ from fintables.api.client import FintablesClient
2
+
3
+ __all__ = ["FintablesClient"]
fintables/api/auth.py ADDED
@@ -0,0 +1,87 @@
1
+ from typing import Any
2
+ from fintables.exceptions import AuthError
3
+
4
+
5
+ async def login(
6
+ client: Any,
7
+ email: str,
8
+ password: str,
9
+ base_url: str = "https://api.fintables.com",
10
+ headers: dict[str, str] | None = None,
11
+ ) -> tuple[str, str]:
12
+ """Logs in using email/password and returns (access_token, refresh_token)."""
13
+ req_headers = {
14
+ "User-Agent": "fintables-mobile/323 (2.28.2)",
15
+ "Content-Type": "application/json",
16
+ "Accept": "application/json",
17
+ }
18
+ if headers:
19
+ req_headers.update(headers)
20
+
21
+ payload = {"email": email, "password": password}
22
+ try:
23
+ resp = await client.post(
24
+ f"{base_url}/auth/token/",
25
+ json=payload,
26
+ headers=req_headers,
27
+ )
28
+ if resp.status_code == 200:
29
+ data = resp.json()
30
+ access = data.get("access") or data.get("access_token")
31
+ refresh = data.get("refresh") or data.get("refresh_token", "")
32
+ if access:
33
+ return access, refresh
34
+ elif resp.status_code == 400 and "username" in getattr(resp, "text", ""):
35
+ resp2 = await client.post(
36
+ f"{base_url}/auth/token/",
37
+ json={"username": email, "password": password},
38
+ headers=req_headers,
39
+ )
40
+ if resp2.status_code == 200:
41
+ data = resp2.json()
42
+ access = data.get("access") or data.get("access_token")
43
+ refresh = data.get("refresh") or data.get("refresh_token", "")
44
+ if access:
45
+ return access, refresh
46
+ except Exception as e:
47
+ if isinstance(e, AuthError):
48
+ raise
49
+ raise AuthError(f"Giriş isteği sırasında hata oluştu: {type(e).__name__}") from e
50
+
51
+ error_msg = resp.text if "resp" in locals() and hasattr(resp, "text") else "Bilinmeyen hata"
52
+ raise AuthError(
53
+ f"Giriş yapılamadı: {error_msg}. "
54
+ "Lütfen FINTABLES_USERNAME/FINTABLES_EMAIL ve FINTABLES_PASSWORD ayarlarınızı kontrol edin."
55
+ )
56
+
57
+
58
+ async def refresh_token(
59
+ client: Any,
60
+ refresh: str,
61
+ base_url: str = "https://api.fintables.com",
62
+ headers: dict[str, str] | None = None,
63
+ ) -> str:
64
+ """Refreshes access_token using refresh_token."""
65
+ req_headers = {
66
+ "User-Agent": "fintables-mobile/323 (2.28.2)",
67
+ "Content-Type": "application/json",
68
+ "Accept": "application/json",
69
+ }
70
+ if headers:
71
+ req_headers.update(headers)
72
+
73
+ try:
74
+ resp = await client.post(
75
+ f"{base_url}/auth/token/refresh/",
76
+ json={"refresh": refresh},
77
+ headers=req_headers,
78
+ )
79
+ if resp.status_code == 200:
80
+ data = resp.json()
81
+ access = data.get("access") or data.get("access_token")
82
+ if access:
83
+ return access
84
+ except Exception as e:
85
+ raise AuthError(f"Token yenileme sırasında hata oluştu: {type(e).__name__}") from e
86
+
87
+ raise AuthError("Token yenilenemedi. Lütfen tekrar giriş yapın.")
@@ -0,0 +1,184 @@
1
+ import inspect
2
+ import logging
3
+ from types import TracebackType
4
+ from typing import Any
5
+ from urllib.parse import urlparse
6
+
7
+ import httpx
8
+
9
+ try:
10
+ from curl_cffi.requests import AsyncSession
11
+ except ImportError:
12
+ AsyncSession = None # type: ignore
13
+
14
+ from fintables.api.auth import login, refresh_token
15
+ from fintables.config import Settings, settings
16
+ from fintables.exceptions import APIError, AuthError, NotFoundError
17
+
18
+
19
+ _logger = logging.getLogger("fintables.client")
20
+ _ALLOWED_HOSTS = {"api.fintables.com", "gate.fintables.com"}
21
+
22
+
23
+ class FintablesClient:
24
+ """Asynchronous HTTP client for Fintables API."""
25
+
26
+ def __init__(
27
+ self,
28
+ custom_settings: Settings | None = None,
29
+ access_token: str | None = None,
30
+ refresh_token_val: str | None = None,
31
+ client: Any = None,
32
+ ):
33
+ self.settings = custom_settings or settings
34
+ self._access_token = access_token
35
+ self._refresh_token = refresh_token_val
36
+ if client is not None:
37
+ self._client = client
38
+ self._owns_client = False
39
+ else:
40
+ if AsyncSession is not None:
41
+ self._client = AsyncSession(impersonate="chrome124", timeout=30.0)
42
+ else:
43
+ self._client = httpx.AsyncClient(timeout=30.0)
44
+ self._owns_client = True
45
+
46
+ async def __aenter__(self) -> "FintablesClient":
47
+ return self
48
+
49
+ async def __aexit__(
50
+ self,
51
+ exc_type: type[BaseException] | None,
52
+ exc_val: BaseException | None,
53
+ exc_tb: TracebackType | None,
54
+ ) -> None:
55
+ await self.close()
56
+
57
+ async def close(self) -> None:
58
+ if self._owns_client:
59
+ if hasattr(self._client, "aclose"):
60
+ await self._client.aclose()
61
+ elif hasattr(self._client, "close"):
62
+ res = self._client.close()
63
+ if inspect.isawaitable(res):
64
+ await res
65
+
66
+ async def _ensure_auth(self) -> None:
67
+ """Ensures that client has a valid access token."""
68
+ if self._access_token:
69
+ return
70
+
71
+ login_id = self.settings.email_or_username
72
+ if login_id and self.settings.password:
73
+ access, refresh = await login(
74
+ self._client,
75
+ login_id,
76
+ self.settings.password,
77
+ base_url=self.settings.base_url,
78
+ headers=self.settings.default_headers,
79
+ )
80
+ self._access_token = access
81
+ self._refresh_token = refresh
82
+ else:
83
+ raise AuthError(
84
+ "Bu işlem için kimlik doğrulama gereklidir. "
85
+ "Lütfen .env dosyasında FINTABLES_USERNAME veya FINTABLES_EMAIL ve FINTABLES_PASSWORD değerlerini ayarlayın."
86
+ )
87
+
88
+ async def _try_refresh_or_relogin(self) -> bool:
89
+ """Attempts to refresh token or re-login. Returns True if succeeded."""
90
+ if self._refresh_token:
91
+ try:
92
+ new_access = await refresh_token(
93
+ self._client,
94
+ self._refresh_token,
95
+ base_url=self.settings.base_url,
96
+ headers=self.settings.default_headers,
97
+ )
98
+ self._access_token = new_access
99
+ return True
100
+ except Exception as e:
101
+ _logger.debug("Token refresh failed: %s", type(e).__name__)
102
+
103
+ login_id = self.settings.email_or_username
104
+ if login_id and self.settings.password:
105
+ try:
106
+ access, refresh = await login(
107
+ self._client,
108
+ login_id,
109
+ self.settings.password,
110
+ base_url=self.settings.base_url,
111
+ headers=self.settings.default_headers,
112
+ )
113
+ self._access_token = access
114
+ self._refresh_token = refresh
115
+ return True
116
+ except Exception as e:
117
+ _logger.debug("Re-login failed: %s", type(e).__name__)
118
+
119
+ return False
120
+
121
+ async def request(
122
+ self,
123
+ method: str,
124
+ path: str,
125
+ *,
126
+ requires_auth: bool = False,
127
+ is_gate: bool = False,
128
+ headers: dict[str, str] | None = None,
129
+ **kwargs: Any,
130
+ ) -> httpx.Response:
131
+ """Sends an HTTP request with automatic headers and authentication handling."""
132
+ base_url = self.settings.gate_url if is_gate else self.settings.base_url
133
+ if path.startswith("http"):
134
+ parsed = urlparse(path)
135
+ if parsed.hostname not in _ALLOWED_HOSTS:
136
+ raise ValueError(f"Disallowed host in URL: {parsed.hostname}")
137
+ url = path
138
+ else:
139
+ url = f"{base_url}{path}"
140
+
141
+ req_headers = dict(self.settings.gate_headers if is_gate else self.settings.default_headers)
142
+ if headers:
143
+ req_headers.update(headers)
144
+
145
+ if requires_auth:
146
+ await self._ensure_auth()
147
+ req_headers["Authorization"] = f"Bearer {self._access_token}"
148
+
149
+ response = await self._client.request(method, url, headers=req_headers, **kwargs) # type: ignore[arg-type]
150
+
151
+ if response.status_code == 401 and requires_auth:
152
+ # Attempt refresh / re-login once
153
+ refreshed = await self._try_refresh_or_relogin()
154
+ if refreshed:
155
+ req_headers["Authorization"] = f"Bearer {self._access_token}"
156
+ response = await self._client.request(method, url, headers=req_headers, **kwargs) # type: ignore[arg-type]
157
+ else:
158
+ raise AuthError("Oturum süresi doldu veya kimlik doğrulama başarısız oldu.")
159
+
160
+ if response.status_code == 404:
161
+ raise NotFoundError(f"İstenen kaynak bulunamadı (404): {url}")
162
+
163
+ if response.status_code >= 400:
164
+ resp_text = response.text
165
+ truncated = (resp_text[:200] + "...") if len(resp_text) > 200 else resp_text
166
+ raise APIError(
167
+ f"API hatası ({response.status_code}): {truncated}",
168
+ status_code=response.status_code,
169
+ response_text=resp_text,
170
+ )
171
+
172
+ return response
173
+
174
+ async def get(self, path: str, **kwargs: Any) -> httpx.Response:
175
+ return await self.request("GET", path, **kwargs)
176
+
177
+ async def post(self, path: str, **kwargs: Any) -> httpx.Response:
178
+ return await self.request("POST", path, **kwargs)
179
+
180
+ async def put(self, path: str, **kwargs: Any) -> httpx.Response:
181
+ return await self.request("PUT", path, **kwargs)
182
+
183
+ async def delete(self, path: str, **kwargs: Any) -> httpx.Response:
184
+ return await self.request("DELETE", path, **kwargs)
@@ -0,0 +1,65 @@
1
+ from fintables.api.endpoints.analyst_ratings import get_analyst_ratings
2
+ from fintables.api.endpoints.agenda import get_agenda
3
+ from fintables.api.endpoints.companies import get_company
4
+ from fintables.api.endpoints.newsletter import get_newsletter, list_newsletters
5
+ from fintables.api.endpoints.post import get_post, list_posts
6
+ from fintables.api.endpoints.video import get_video, list_videos
7
+ from fintables.api.endpoints.feed import get_feed, get_topic_feed
8
+ from fintables.api.endpoints.memo import (
9
+ create_memo,
10
+ delete_memo,
11
+ list_memos,
12
+ update_memo,
13
+ )
14
+ from fintables.api.endpoints.search import search
15
+ from fintables.api.endpoints.sheets import get_sheets
16
+ from fintables.api.endpoints.symbols import get_symbol_summary
17
+ from fintables.api.endpoints.watchlist import add_favorite, remove_favorite
18
+ from fintables.api.endpoints.funds import get_fund, get_fund_info
19
+ from fintables.api.endpoints.portfolio import (
20
+ add_transaction,
21
+ create_portfolio,
22
+ delete_portfolio,
23
+ get_positions,
24
+ list_portfolios,
25
+ update_portfolio,
26
+ )
27
+ from fintables.api.endpoints.notifications import (
28
+ get_unread_status,
29
+ list_notifications,
30
+ mark_notifications_as_read,
31
+ )
32
+
33
+ __all__ = [
34
+ "get_company",
35
+ "get_symbol_summary",
36
+ "get_analyst_ratings",
37
+ "get_agenda",
38
+ "get_newsletter",
39
+ "list_newsletters",
40
+ "get_post",
41
+ "list_posts",
42
+ "get_video",
43
+ "list_videos",
44
+ "search",
45
+ "get_sheets",
46
+ "get_feed",
47
+ "get_topic_feed",
48
+ "add_favorite",
49
+ "remove_favorite",
50
+ "list_memos",
51
+ "create_memo",
52
+ "update_memo",
53
+ "delete_memo",
54
+ "get_fund",
55
+ "get_fund_info",
56
+ "list_portfolios",
57
+ "create_portfolio",
58
+ "delete_portfolio",
59
+ "update_portfolio",
60
+ "get_positions",
61
+ "add_transaction",
62
+ "list_notifications",
63
+ "get_unread_status",
64
+ "mark_notifications_as_read",
65
+ ]
@@ -0,0 +1,23 @@
1
+ from typing import Literal
2
+
3
+ from fintables.api.client import FintablesClient
4
+ from fintables.models.agenda import AgendaItem
5
+
6
+ AgendaTime = Literal["today", "thisWeek", "nextWeek"]
7
+
8
+
9
+ async def get_agenda(
10
+ client: FintablesClient,
11
+ time: AgendaTime = "today",
12
+ ) -> list[AgendaItem]:
13
+ """Ajanda etkinliklerini getirir (bugün / bu hafta / gelecek hafta)."""
14
+ response = await client.get(
15
+ "/mobile/agenda/",
16
+ params={"time": time},
17
+ requires_auth=True,
18
+ )
19
+ data = response.json()
20
+ if isinstance(data, list):
21
+ return [AgendaItem.model_validate(item) for item in data]
22
+ # API ileride sayfalı yanıt dönerse results listesini kullan
23
+ return [AgendaItem.model_validate(item) for item in data.get("results", [])]
@@ -0,0 +1,26 @@
1
+ from fintables.api.client import FintablesClient
2
+ from fintables.models.analyst_rating import AnalystRatingList
3
+
4
+
5
+ async def get_analyst_ratings(
6
+ client: FintablesClient,
7
+ ticker: str,
8
+ brokerage_id: str | None = None,
9
+ in_model_portfolio: bool | None = None,
10
+ ) -> AnalystRatingList:
11
+ """Fetches analyst ratings for a given symbol."""
12
+ ticker_clean = ticker.strip().upper()
13
+ params: dict[str, str] = {
14
+ "code": ticker_clean,
15
+ "brokerage_id": brokerage_id or "",
16
+ }
17
+ if in_model_portfolio is not None:
18
+ params["in_model_portfolio"] = "true" if in_model_portfolio else "false"
19
+ else:
20
+ params["in_model_portfolio"] = ""
21
+
22
+ response = await client.get("/analyst-ratings/", params=params)
23
+ data = response.json()
24
+ if isinstance(data, list):
25
+ return AnalystRatingList(results=data)
26
+ return AnalystRatingList.model_validate(data)
@@ -0,0 +1,9 @@
1
+ from fintables.api.client import FintablesClient
2
+ from fintables.models.company import Company
3
+
4
+
5
+ async def get_company(client: FintablesClient, ticker: str) -> Company:
6
+ """Fetches company profile and ratio types."""
7
+ ticker_clean = ticker.strip().upper()
8
+ response = await client.get(f"/companies/{ticker_clean}/")
9
+ return Company.model_validate(response.json())
@@ -0,0 +1,47 @@
1
+ from fintables.api.client import FintablesClient
2
+ from fintables.models.feed import FeedPage
3
+
4
+
5
+ async def get_feed(
6
+ client: FintablesClient,
7
+ ticker: str,
8
+ page_size: int = 30,
9
+ cursor: str | None = None,
10
+ ) -> FeedPage:
11
+ """Fetches topic feed (news, posts, newsletter, articles) for a given symbol."""
12
+ ticker_clean = ticker.strip().upper()
13
+ params: dict[str, str | int] = {
14
+ "symbols": ticker_clean,
15
+ "for_everyone": 0,
16
+ "page_size": page_size,
17
+ }
18
+ if cursor:
19
+ params["cursor"] = cursor
20
+
21
+ response = await client.get(
22
+ "/topic-feed/",
23
+ params=params,
24
+ requires_auth=True,
25
+ )
26
+ return FeedPage.model_validate(response.json())
27
+
28
+
29
+ async def get_topic_feed(
30
+ client: FintablesClient,
31
+ page_size: int = 100,
32
+ cursor: str | None = None,
33
+ ) -> FeedPage:
34
+ """Fetches global topic feed without a specific symbol."""
35
+ params: dict[str, str | int] = {
36
+ "only_pro": 1,
37
+ "page_size": page_size,
38
+ }
39
+ if cursor:
40
+ params["cursor"] = cursor
41
+
42
+ response = await client.get(
43
+ "/topic-feed/",
44
+ params=params,
45
+ requires_auth=True,
46
+ )
47
+ return FeedPage.model_validate(response.json())
@@ -0,0 +1,14 @@
1
+ from fintables.api.client import FintablesClient
2
+ from fintables.models.fund import Fund, FundInfo
3
+
4
+ async def get_fund(client: FintablesClient, code: str) -> Fund:
5
+ """Fetches general information about a fund."""
6
+ code_clean = code.strip().upper()
7
+ response = await client.get(f"/funds/{code_clean}/")
8
+ return Fund.model_validate(response.json())
9
+
10
+ async def get_fund_info(client: FintablesClient, code: str) -> FundInfo:
11
+ """Fetches detailed information and portfolio of a fund."""
12
+ code_clean = code.strip().upper()
13
+ response = await client.get(f"/funds/{code_clean}/info/")
14
+ return FundInfo.model_validate(response.json())
@@ -0,0 +1,36 @@
1
+ from fintables.api.client import FintablesClient
2
+ from fintables.models.memo import Memo
3
+
4
+
5
+ async def list_memos(client: FintablesClient) -> list[Memo]:
6
+ """Lists all memos for the authenticated user."""
7
+ response = await client.get("/memos/", requires_auth=True)
8
+ data = response.json()
9
+ return [Memo.model_validate(item) for item in data]
10
+
11
+
12
+ async def create_memo(client: FintablesClient, code: str, content: str) -> Memo:
13
+ """Creates a new memo for a symbol."""
14
+ code_clean = code.strip().upper()
15
+ response = await client.post(
16
+ "/memos/",
17
+ json={"code": code_clean, "content": content},
18
+ requires_auth=True,
19
+ )
20
+ return Memo.model_validate(response.json())
21
+
22
+
23
+ async def update_memo(client: FintablesClient, memo_id: int, code: str, content: str) -> Memo:
24
+ """Updates an existing memo."""
25
+ code_clean = code.strip().upper()
26
+ response = await client.put(
27
+ f"/memos/{memo_id}/",
28
+ json={"id": memo_id, "code": code_clean, "content": content},
29
+ requires_auth=True,
30
+ )
31
+ return Memo.model_validate(response.json())
32
+
33
+
34
+ async def delete_memo(client: FintablesClient, memo_id: int) -> None:
35
+ """Deletes a memo by ID."""
36
+ await client.delete(f"/memos/{memo_id}/", requires_auth=True)
@@ -0,0 +1,36 @@
1
+ from fintables.api.client import FintablesClient
2
+ from fintables.models.newsletter import NewsletterDetail, NewsletterPage
3
+
4
+
5
+ async def list_newsletters(
6
+ client: FintablesClient,
7
+ main_category: str = "bist",
8
+ page_size: int = 100,
9
+ page: int | None = None,
10
+ ) -> NewsletterPage:
11
+ """Bültenleri listeler."""
12
+ params: dict[str, str | int] = {
13
+ "main_category": main_category,
14
+ "page_size": page_size,
15
+ }
16
+ if page is not None:
17
+ params["page"] = page
18
+
19
+ response = await client.get(
20
+ "/newsletters/",
21
+ params=params,
22
+ requires_auth=True,
23
+ )
24
+ return NewsletterPage.model_validate(response.json())
25
+
26
+
27
+ async def get_newsletter(
28
+ client: FintablesClient,
29
+ slug: str,
30
+ ) -> NewsletterDetail:
31
+ """Bülten detayını (içeriğiyle birlikte) getirir."""
32
+ response = await client.get(
33
+ f"/newsletters/{slug}/",
34
+ requires_auth=True,
35
+ )
36
+ return NewsletterDetail.model_validate(response.json())
@@ -0,0 +1,41 @@
1
+ from fintables.api.client import FintablesClient
2
+ from fintables.models.notification import (
3
+ MarkAsReadResponse,
4
+ NotificationPage,
5
+ NotificationUnreadStatus,
6
+ )
7
+
8
+
9
+ async def list_notifications(
10
+ client: FintablesClient,
11
+ page_size: int = 100,
12
+ ) -> NotificationPage:
13
+ """Fetches user notifications."""
14
+ params: dict[str, str | int] = {
15
+ "page_size": page_size,
16
+ }
17
+
18
+ response = await client.get(
19
+ "/notifications/",
20
+ params=params,
21
+ requires_auth=True,
22
+ )
23
+ return NotificationPage.model_validate(response.json())
24
+
25
+
26
+ async def get_unread_status(client: FintablesClient) -> NotificationUnreadStatus:
27
+ """Checks whether the user has unread notifications and returns the last read timestamp."""
28
+ response = await client.get(
29
+ "/notifications/unread_count/",
30
+ requires_auth=True,
31
+ )
32
+ return NotificationUnreadStatus.model_validate(response.json())
33
+
34
+
35
+ async def mark_notifications_as_read(client: FintablesClient) -> MarkAsReadResponse:
36
+ """Marks all user notifications as read."""
37
+ response = await client.post(
38
+ "/notifications/mark_as_read/",
39
+ requires_auth=True,
40
+ )
41
+ return MarkAsReadResponse.model_validate(response.json())
@@ -0,0 +1,95 @@
1
+ from fintables.api.client import FintablesClient
2
+ from fintables.models.portfolio import Portfolio, PortfolioPage, PositionPage, Transaction
3
+
4
+
5
+ async def list_portfolios(
6
+ client: FintablesClient,
7
+ page_size: int = 100,
8
+ ) -> PortfolioPage:
9
+ """Kullanıcının sanal portföylerini listeler."""
10
+ response = await client.get(
11
+ "/portfolios/",
12
+ params={"page_size": page_size},
13
+ requires_auth=True,
14
+ )
15
+ return PortfolioPage.model_validate(response.json())
16
+
17
+
18
+ async def create_portfolio(
19
+ client: FintablesClient,
20
+ title: str,
21
+ ) -> Portfolio:
22
+ """Yeni bir sanal portföy oluşturur."""
23
+ response = await client.post(
24
+ "/portfolios/",
25
+ json={"title": title},
26
+ requires_auth=True,
27
+ )
28
+ return Portfolio.model_validate(response.json())
29
+
30
+
31
+ async def delete_portfolio(
32
+ client: FintablesClient,
33
+ portfolio_id: str,
34
+ ) -> None:
35
+ """Belirtilen sanal portföyü siler."""
36
+ await client.delete(
37
+ f"/portfolios/{portfolio_id}/",
38
+ requires_auth=True,
39
+ )
40
+
41
+
42
+ async def update_portfolio(
43
+ client: FintablesClient,
44
+ portfolio_id: str,
45
+ new_title: str,
46
+ ) -> Portfolio:
47
+ """Portföy adını günceller."""
48
+ response = await client.request(
49
+ "PATCH",
50
+ f"/portfolios/{portfolio_id}/",
51
+ json={"id": portfolio_id, "title": new_title},
52
+ requires_auth=True,
53
+ )
54
+ return Portfolio.model_validate(response.json())
55
+
56
+
57
+ async def get_positions(
58
+ client: FintablesClient,
59
+ portfolio_id: str,
60
+ ) -> PositionPage:
61
+ """Portföydeki pozisyonları (varlıkları) getirir."""
62
+ response = await client.get(
63
+ f"/portfolios/{portfolio_id}/positions/",
64
+ requires_auth=True,
65
+ )
66
+ data = response.json()
67
+ # API doğrudan liste de döndürebilir
68
+ if isinstance(data, list):
69
+ data = {"results": data}
70
+ return PositionPage.model_validate(data)
71
+
72
+
73
+ async def add_transaction(
74
+ client: FintablesClient,
75
+ portfolio_id: str,
76
+ code: str,
77
+ side: str,
78
+ amount: str | int | float,
79
+ price: str | float,
80
+ date: str,
81
+ ) -> Transaction:
82
+ """Portföye alış (BUY) veya satış (SELL) işlemi ekler."""
83
+ payload = {
84
+ "code": code.strip().upper(),
85
+ "side": side.upper(),
86
+ "amount": str(amount),
87
+ "price": str(price),
88
+ "date": date,
89
+ }
90
+ response = await client.post(
91
+ f"/portfolios/{portfolio_id}/transactions/",
92
+ json=payload,
93
+ requires_auth=True,
94
+ )
95
+ return Transaction.model_validate(response.json())