async-universalis 3.0.2.dev0__tar.gz → 4.0.2.dev0__tar.gz

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 (17) hide show
  1. {async_universalis-3.0.2.dev0/async_universalis.egg-info → async_universalis-4.0.2.dev0}/PKG-INFO +1 -1
  2. {async_universalis-3.0.2.dev0 → async_universalis-4.0.2.dev0}/async_universalis/__init__.py +143 -41
  3. {async_universalis-3.0.2.dev0 → async_universalis-4.0.2.dev0/async_universalis.egg-info}/PKG-INFO +1 -1
  4. {async_universalis-3.0.2.dev0 → async_universalis-4.0.2.dev0}/LICENSE +0 -0
  5. {async_universalis-3.0.2.dev0 → async_universalis-4.0.2.dev0}/MANIFEST.in +0 -0
  6. {async_universalis-3.0.2.dev0 → async_universalis-4.0.2.dev0}/README.md +0 -0
  7. {async_universalis-3.0.2.dev0 → async_universalis-4.0.2.dev0}/async_universalis/_enums.py +0 -0
  8. {async_universalis-3.0.2.dev0 → async_universalis-4.0.2.dev0}/async_universalis/_types.py +0 -0
  9. {async_universalis-3.0.2.dev0 → async_universalis-4.0.2.dev0}/async_universalis/errors.py +0 -0
  10. {async_universalis-3.0.2.dev0 → async_universalis-4.0.2.dev0}/async_universalis/items.json +0 -0
  11. {async_universalis-3.0.2.dev0 → async_universalis-4.0.2.dev0}/async_universalis/py.typed +0 -0
  12. {async_universalis-3.0.2.dev0 → async_universalis-4.0.2.dev0}/async_universalis.egg-info/SOURCES.txt +0 -0
  13. {async_universalis-3.0.2.dev0 → async_universalis-4.0.2.dev0}/async_universalis.egg-info/dependency_links.txt +0 -0
  14. {async_universalis-3.0.2.dev0 → async_universalis-4.0.2.dev0}/async_universalis.egg-info/requires.txt +0 -0
  15. {async_universalis-3.0.2.dev0 → async_universalis-4.0.2.dev0}/async_universalis.egg-info/top_level.txt +0 -0
  16. {async_universalis-3.0.2.dev0 → async_universalis-4.0.2.dev0}/pyproject.toml +0 -0
  17. {async_universalis-3.0.2.dev0 → async_universalis-4.0.2.dev0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: async_universalis
3
- Version: 3.0.2.dev0
3
+ Version: 4.0.2.dev0
4
4
  Summary: A bare-bones wrapper package to utilitize Universalis API in python.
5
5
  Author-email: k8thekat <Cadwalladerkatelynn@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -23,7 +23,7 @@ from __future__ import annotations
23
23
  __title__ = "Universalis API wrapper"
24
24
  __author__ = "k8thekat"
25
25
  __license__ = "GNU"
26
- __version__ = "3.0.2-dev"
26
+ __version__ = "4.0.2-dev"
27
27
  __credits__ = "Universalis and Square Enix"
28
28
 
29
29
 
@@ -31,7 +31,7 @@ import datetime
31
31
  import json
32
32
  import logging
33
33
  import pathlib
34
- from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Optional, Self, Union
34
+ from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Optional, Self, Union, Unpack
35
35
 
36
36
  import aiohttp
37
37
 
@@ -43,9 +43,10 @@ from .errors import UniversalisError
43
43
  if TYPE_CHECKING:
44
44
  import types
45
45
 
46
- from _types import *
47
46
  from aiohttp.client import _RequestOptions as AiohttpRequestOptions # pyright: ignore[reportPrivateUsage]
48
47
 
48
+ from ._types import *
49
+
49
50
  DataTypedAliase = Union[CurrentListing, CurrentDCWorld, HistoryDCWorld, HistoryEntries]
50
51
 
51
52
 
@@ -240,14 +241,6 @@ class UniversalisAPI:
240
241
  data = await session.get(url=url, **request_params)
241
242
 
242
243
  LOGGER.debug("<%s._request> | Status Code: %s | Content Type: %s", __class__.__name__, data.status, data.content_type)
243
- if not 200 <= data.status < 300:
244
- raise UniversalisError(data.status, url, "generic http request")
245
- if data.status == 400:
246
- raise UniversalisError(
247
- data.status,
248
- url,
249
- "invalid parameters",
250
- )
251
244
  # 404 - The world/DC or item requested is invalid. When requesting multiple items at once, an invalid item ID will not trigger this.
252
245
  # Instead, the returned list of unresolved item IDs will contain the invalid item ID or IDs.
253
246
  if data.status == 404:
@@ -256,7 +249,14 @@ class UniversalisAPI:
256
249
  url,
257
250
  "invalid World/DC or Item ID",
258
251
  )
259
-
252
+ if data.status == 400:
253
+ raise UniversalisError(
254
+ data.status,
255
+ url,
256
+ "invalid parameters",
257
+ )
258
+ if not 200 <= data.status < 300:
259
+ raise UniversalisError(data.status, url, "generic http request")
260
260
  self.api_call_time = datetime.datetime.now(datetime.UTC)
261
261
  res: Any = await data.json()
262
262
  return res
@@ -283,11 +283,11 @@ class UniversalisAPI:
283
283
 
284
284
 
285
285
  .. note::
286
- - If you specify a `<World>`.
286
+ - If you specify a :class:`World` when getting marketboard data..
287
287
  - All `<CurrentData.listings>` and `<CurrentData.recent_history>` will not have the attributes `world_name`.
288
- - If you specify a `<DataCenter>`.
288
+ - If you specify a :class:`DataCenter` when getting marketboard data...
289
289
  - All `<CurrentData.listings>` and `<CurrentData.recent_history>` will have the `world_id` and `world_name` attributes.
290
- - `<CurrentData>` will also have an additional attribute called `dc_name`.
290
+ - :class:`CurrentData` will also have an additional attribute called `dc_name`.
291
291
 
292
292
  .. note::
293
293
  You can change the default DataCenter by setting the `<UniversalisAPI>.datacenter` property.
@@ -345,7 +345,7 @@ class UniversalisAPI:
345
345
  num_history_entries: int = 10,
346
346
  item_quality: ItemQuality = ItemQuality.NQ,
347
347
  trim_item_fields: bool = False,
348
- ) -> list[CurrentData] | CurrentData:
348
+ ) -> CurrentData | MultiPart | None:
349
349
  """Retrieve a bulk item search of Universalis marketboard data.
350
350
 
351
351
  Retrieves the data currently shown on the market board for the requested item and world or data center.
@@ -358,11 +358,11 @@ class UniversalisAPI:
358
358
  - See `https://docs.universalis.app/` and use their forms to generate a string with the fields you want.
359
359
 
360
360
  .. note::
361
- - If you specify a `<World>`.
361
+ - If you specify a :class:`World` when getting marketboard data..
362
362
  - All `<CurrentData.listings>` and `<CurrentData.recent_history>` will not have the attributes `world_name`.
363
- - If you specify a `<DataCenter>`.
363
+ - If you specify a :class:`DataCenter` when getting marketboard data...
364
364
  - All `<CurrentData.listings>` and `<CurrentData.recent_history>` will have the `world_id` and `world_name` attributes.
365
- - `<CurrentData>` will also have an additional attribute called `dc_name`.
365
+ - :class:`CurrentData` will also have an additional attribute called `dc_name`.
366
366
 
367
367
 
368
368
  .. note::
@@ -424,7 +424,8 @@ class UniversalisAPI:
424
424
  trim_item_fields=trim_item_fields,
425
425
  )
426
426
 
427
- results: list[CurrentData] = []
427
+ # results: list[CurrentData] = []
428
+ data: Optional[MultiPart] = None
428
429
  for idx in range(0, len(query), 100):
429
430
  api_url: str = (
430
431
  f"{self.base_api_url}/{world_or_dc.name}/{','.join(query[idx : idx + 100])}?listings={num_listings}"
@@ -435,13 +436,23 @@ class UniversalisAPI:
435
436
  api_url += self.multi_item_fields
436
437
 
437
438
  res: MultiPartData = await self._request(url=api_url)
438
- # If we use a Datacenter, we will have `world_name` and `world_id` inside our Listings(CurrentDataEntries).
439
- # If we use a World, it will be at the topmost level(CurrentData) of the results and no where else.
440
- # TODO(@k8thekat): - If we have the world keys @CurrentData level, we should pass the value into CurrentDataEntries
441
439
  LOGGER.debug("<%s._get_bulk_current_data>. | DC/World: %s | Num of Items: %s", __class__.__name__, world_or_dc.name, len(items))
442
440
  LOGGER.debug("<%s._get_bulk_current_data>. | URL: %s | Response:\n%s", __class__.__name__, api_url, res)
443
- results.extend([CurrentData(universalis=self, data=value) for value in res.get("items").values() if "listings" in value])
444
- return results
441
+
442
+ # results.extend([CurrentData(universalis=self, data=value) for value in res.get("items").values() if "listings" in value])
443
+ if data is None:
444
+ data = MultiPart(
445
+ universalis=self,
446
+ resolved_items=[
447
+ CurrentData(universalis=self, data=value) for value in res.get("items").values() if "listings" in value
448
+ ],
449
+ **res,
450
+ )
451
+ else:
452
+ data.items.extend([CurrentData(universalis=self, data=value) for value in res.get("items").values() if "listings" in value])
453
+ data.unresolved_items.extend(res["unresolvedItems"])
454
+
455
+ return data
445
456
 
446
457
  async def get_history_data(
447
458
  self,
@@ -465,9 +476,9 @@ class UniversalisAPI:
465
476
 
466
477
 
467
478
  .. note::
468
- - If you specify a `<World>`.
479
+ - If you specify a :class:`World` when getting marketboard data..
469
480
  - All `<HistoryData.entries>` will not have the attributes `world_name`.
470
- - If you specify a `<DataCenter>`.
481
+ - If you specify a :class:`DataCenter` when getting marketboard data...
471
482
  - All `<HistoryData.entries>` will have the `world_id` and `world_name` attributes.
472
483
  - `<HistoryData>` will also have an additional attribute called `dc_name`.
473
484
 
@@ -520,7 +531,7 @@ class UniversalisAPI:
520
531
  min_price: int = 0,
521
532
  max_price: int = 2147483647,
522
533
  history: int = 604800000,
523
- ) -> list[HistoryData] | HistoryData:
534
+ ) -> HistoryData | MultiPart | None:
524
535
  """Retrieve the Universalis marketboard history data for the provided item.
525
536
 
526
537
  Retrieves the history data for the requested item and world or data center.
@@ -533,9 +544,9 @@ class UniversalisAPI:
533
544
 
534
545
 
535
546
  .. note::
536
- - If you specify a `<World>`.
547
+ - If you specify a :class:`World` when getting marketboard data..
537
548
  - All `<HistoryData.entries>` will not have the attributes `world_name`.
538
- - If you specify a `<DataCenter>`.
549
+ - If you specify a :class:`DataCenter` when getting marketboard data...
539
550
  - All `<HistoryData.entries>` will have the `world_id` and `world_name` attributes.
540
551
  - `<HistoryData>` will also have an additional attribute called `dc_name`.
541
552
 
@@ -587,7 +598,7 @@ class UniversalisAPI:
587
598
  if world_or_dc is None:
588
599
  world_or_dc = self.default_datacenter
589
600
 
590
- # If we are given a single entry in our list; use the `get_current_data` instead.
601
+ # If we are given a single entry in our list; use the `get_history_data` instead.
591
602
  # We could modify the `join` statement below; but this is far easier and provides the same results.
592
603
  # So if the `dcName` key exists, we searched by a DataCenter.
593
604
  # otherwise the `worldName` and `worldID` key will exist.
@@ -601,7 +612,8 @@ class UniversalisAPI:
601
612
  history=history,
602
613
  )
603
614
 
604
- results: list[HistoryData] = []
615
+ # results: list[HistoryData] = []
616
+ data: Optional[MultiPart] = None
605
617
  for idx in range(0, len(query), 100):
606
618
  api_url: str = (
607
619
  f"{self.base_api_url}/history/{world_or_dc.name}/{','.join(query[idx : idx + 100])}?entriesToReturn={num_listings}"
@@ -616,8 +628,17 @@ class UniversalisAPI:
616
628
  len(items),
617
629
  res,
618
630
  )
619
- results.extend(HistoryData(universalis=self, data=value) for value in res.get("items").values() if "entries" in value)
620
- return results
631
+ # results.extend(HistoryData(universalis=self, data=value) for value in res.get("items").values() if "entries" in value)
632
+ if data is None:
633
+ data = MultiPart(
634
+ universalis=self,
635
+ resolved_items=[HistoryData(universalis=self, data=value) for value in res.get("items").values() if "entries" in value],
636
+ **res,
637
+ )
638
+ else:
639
+ data.items.extend([HistoryData(universalis=self, data=value) for value in res.get("items").values() if "entries" in value])
640
+ data.unresolved_items.extend(res["unresolvedItems"])
641
+ return data
621
642
 
622
643
  @staticmethod
623
644
  def from_camel_case(
@@ -699,12 +720,12 @@ class Generic:
699
720
  _repr_keys: list[str]
700
721
 
701
722
  world_id: Optional[int]
702
- world_name: Optional[str]
723
+ # world_name: Optional[str]
703
724
  # This value only exists if you look up results by "Datacenter" instead of "World"
704
725
  dc_name: Optional[str]
705
- _raw: DataTypedAliase
726
+ _raw: DataTypedAliase | MultiPartData
706
727
 
707
- def __init__(self, data: DataTypedAliase) -> None:
728
+ def __init__(self, data: DataTypedAliase | MultiPartData) -> None:
708
729
  LOGGER.debug("<%s.__init__()> data: %s", __class__.__name__, data)
709
730
  self._raw = data
710
731
 
@@ -721,6 +742,23 @@ class Generic:
721
742
  f"{e}: {getattr(self, e)}" for e in sorted(self.__dict__) if e.startswith("_") is False
722
743
  ])
723
744
 
745
+ @property
746
+ def world_name(self) -> Optional[str]:
747
+ """The Final Fantasy 14 World name, if applicable.
748
+
749
+ .. note::
750
+ - If you specify a :class:`World` when getting marketboard data..
751
+ - All `<CurrentData.listings>` and `<CurrentData.recent_history>` will not have the attributes `world_name`.
752
+ - If you specify a :class:`DataCenter` when getting marketboard data...
753
+ - All `<CurrentData.listings>` and `<CurrentData.recent_history>` will have the `world_id` and `world_name` attributes.
754
+ - :class:`CurrentData` will also have an additional attribute called `dc_name`.
755
+ """
756
+ return self._world_name
757
+
758
+ @world_name.setter
759
+ def world_name(self, value: Optional[str]) -> None:
760
+ self._world_name: Optional[str] = value
761
+
724
762
 
725
763
  class GenericData(Generic):
726
764
  """Base class for mutual attributes and properties for Universalis data.
@@ -904,10 +942,11 @@ class CurrentData(GenericData):
904
942
  "min_price",
905
943
  "listings_count",
906
944
  "recent_history_count",
907
- "listings",
908
- "recent_history",
945
+ # "listings", # !This floods any prints.
946
+ # "recent_history", # !This floods any prints.
909
947
  "dc_name",
910
948
  ]
949
+
911
950
  # We get it early here, as the for loop won't set it to `None` if the data isn't there.
912
951
  # This is being used for `CurrentDataEntries` as fetching "world" data doesn't provide the field to `listings`.
913
952
  self.world_name = data.get("worldName", None)
@@ -918,8 +957,13 @@ class CurrentData(GenericData):
918
957
  if isinstance(value, list) and key.lower() == "listings":
919
958
  self.listings = value
920
959
 
960
+ # This should handle price formatting.
961
+ # elif "price" in key.lower() and (isinstance(value, (int, float))):
962
+ # setattr(self, key, f"{round(value):,d}")
963
+
921
964
  elif key.lower() == "has_data" and isinstance(value, int):
922
965
  self.has_data = bool(value)
966
+
923
967
  else:
924
968
  setattr(self, key, value)
925
969
  self.name = self._universalis._get_item(self.item_id) # type: ignore[reportPrivateUsage] # noqa: SLF001
@@ -935,7 +979,7 @@ class CurrentData(GenericData):
935
979
 
936
980
  @property
937
981
  def recent_history(self) -> list[HistoryDataEntries]:
938
- """The currently-shown sales, sorted by `timestamp`."""
982
+ """The most recent sales, sorted by `timestamp`."""
939
983
  return self._recent_history
940
984
 
941
985
  @recent_history.setter
@@ -1041,6 +1085,11 @@ class CurrentDataEntries(Generic):
1041
1085
  key = UniversalisAPI.from_camel_case(key_name=key_)
1042
1086
  if key.lower() in {"on_mannequin", "is_crafted", "hq"} and isinstance(value, int):
1043
1087
  setattr(self, key, bool(value))
1088
+
1089
+ # This should handle price formatting.
1090
+ # elif isinstance(value, (int, float)) and ("price" in key.lower() or key.lower() == "total" or key.lower() == "tax"):
1091
+ # setattr(self, key, f"{round(value):,d}")
1092
+
1044
1093
  else:
1045
1094
  setattr(self, key, value)
1046
1095
 
@@ -1185,7 +1234,7 @@ class HistoryData(GenericData):
1185
1234
  """
1186
1235
  super().__init__(data=data)
1187
1236
  self._universalis = universalis
1188
- self._repr_keys = ["world_name", "dc_name", "item_id", "last_upload_time", "entries"]
1237
+ self._repr_keys = ["world_name", "dc_name", "item_id", "last_upload_time"] # "entries" - Removed to prevent flooding the console.
1189
1238
 
1190
1239
  # We get it early here, as the for loop won't set it to `None` if the data isn't there.
1191
1240
  # This is being used for `CurrentDataEntries` as fetching "world" data doesn't provide the field to `listings`.
@@ -1195,6 +1244,9 @@ class HistoryData(GenericData):
1195
1244
  key: str = UniversalisAPI.from_camel_case(key_name=key_)
1196
1245
  if key.lower() == "entries" and isinstance(value, list):
1197
1246
  self.entries = value
1247
+ # This should handle price formatting.
1248
+ # elif isinstance(value, (int, float)) and "velocity" in key:
1249
+ # setattr(self, key, f"{round(value):,d}")
1198
1250
  else:
1199
1251
  setattr(self, key, value)
1200
1252
  self.name = self._universalis._get_item(self.item_id) # type: ignore[reportPrivateUsage] # noqa: SLF001
@@ -1273,6 +1325,10 @@ class HistoryDataEntries(Generic):
1273
1325
  key: str = UniversalisAPI.from_camel_case(key_name=key_)
1274
1326
  if key.lower() in {"hq", "on_mannequin"} and isinstance(value, int):
1275
1327
  setattr(self, key, bool(value))
1328
+
1329
+ # This should handle price formatting.
1330
+ # elif isinstance(value, (int, float)) and "price" in key:
1331
+ # setattr(self, key, f"{round(value):,d}")
1276
1332
  else:
1277
1333
  setattr(self, key, value)
1278
1334
 
@@ -1331,3 +1387,49 @@ class HistoryDataEntries(Generic):
1331
1387
  self._timestamp = datetime.datetime.fromtimestamp(timestamp=value, tz=datetime.UTC)
1332
1388
  except ValueError:
1333
1389
  self._timestamp = value
1390
+
1391
+
1392
+ class MultiPart(Generic):
1393
+ """A represensation of a Universalis API response.
1394
+
1395
+ Attributes
1396
+ ----------
1397
+ items: :class:`list[HistoryData | CurrentData]`
1398
+ A list of either :class:`HistoryData` or :class:`CurrentData`.
1399
+ raw_items: :class:`dict[str, CurrentDCWorld | HistoryDCWorld]`
1400
+ The JSON response data for each item in `<MultiPartData>.items`.
1401
+ item_ids: :class:`list[int]`
1402
+ The list of item IDs.
1403
+ unresolved_items: :class:`list[int]`
1404
+ The list of unresolved item IDs.
1405
+
1406
+ """
1407
+
1408
+ items: list[HistoryData | CurrentData]
1409
+ raw_items: dict[str, CurrentDCWorld | HistoryDCWorld]
1410
+ item_ids: list[int]
1411
+ unresolved_items: list[int]
1412
+
1413
+ __slots__ = ["item_ids", "items", "resolved_items", "unresolved_items"]
1414
+
1415
+ def __init__(self, universalis: UniversalisAPI, resolved_items: list[HistoryData | CurrentData], **data: Unpack[MultiPartData]) -> None:
1416
+ """Build your JSON response :class:`MultiPart`.
1417
+
1418
+ Parameters
1419
+ ----------
1420
+ universalis: :class:`UniversalisAPI`
1421
+ A reference to the :class:`UniversalisAPI` object.
1422
+ resolved_items: :class:`list[HistoryData | CurrentData]`
1423
+ The data set built from `<MultiPartData>.items` as a list of either :class:`HistoryData` or :class:`CurrentData`.
1424
+ **data: :class:`MultiPartData`
1425
+ The JSON response data as a dict.
1426
+
1427
+ """
1428
+ super().__init__(data=data)
1429
+ self._universalis: UniversalisAPI = universalis
1430
+ self._repr_keys = ["item_ids", "unresolved_items"]
1431
+
1432
+ self.item_ids = data["itemIDs"]
1433
+ self.unresolved_items = data["unresolvedItems"]
1434
+ self.raw_items = data["items"]
1435
+ self.items = resolved_items
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: async_universalis
3
- Version: 3.0.2.dev0
3
+ Version: 4.0.2.dev0
4
4
  Summary: A bare-bones wrapper package to utilitize Universalis API in python.
5
5
  Author-email: k8thekat <Cadwalladerkatelynn@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE