# -*- coding: utf-8 -*-
from setuptools import setup

packages = \
['ps2_census']

package_data = \
{'': ['*']}

install_requires = \
['requests>=2.23,<3.0', 'toml>=0.9,<0.10', 'websockets>=8.1,<9.0']

setup_kwargs = {
    'name': 'ps2-census',
    'version': '0.10.3',
    'description': 'Daybreak Games Census API client for Planetside 2 (UNOFFICIAL)',
    'long_description': '# ps2-census\n\n*ps2-census* is a low-level client for Daybreak\'s Planetside 2 Census API written in Python >= 3.8.\n\n   * [ps2-census](#ps2-census)\n      * [Installation](#installation)\n      * [Full examples](#full-examples)\n      * [Query building](#query-building)\n         * [Basic query](#basic-query)\n         * [Simple join](#simple-join)\n         * [Nesting joins](#nesting-joins)\n            * [Deeply nested join](#deeply-nested-join)\n            * [Lateraly nested joins](#lateraly-nested-joins)\n         * [Tree](#tree)\n      * [Factories](#factories)\n      * [Common enums](#common-enums)\n      * [Event Stream](#event-stream)\n         * [Usage](#usage)\n      * [Next steps](#next-steps)\n      * [Similar projects](#similar-projects)\n      * [Development](#development)\n         * [Environment](#environment)\n         * [Technical details](#technical-details)\n\n*Features*:\n- Build queries through method chaining\n- Join collections and nest them through method chaining\n- Get raw (deserialized) responses as Python dictionaries\n- Access common enums directly\n- Subscribe to event streams\n\nBy default the `example` service ID is used; however it is not recommended for production.\nYou should get your own service ID from the webside below and supply it to the client whenever needed.\n\nMore information about the Census API is available on the official Census documentation [here](http://census.daybreakgames.com/).\n\n[Iridar\'s blog](https://iridar-mirror.knyazev.io/index.html%3Fp=4127.html) is also recommended to understand\nthe quirks of this particular API as this knowledge is necessary to use *ps2-census* properly.\n\n## Installation\n\n```sh\npip install ps2-census\n```\n\n## Full examples\n\nBefore diving in the details of the *ps2-census* client, full examples are available in the `examples` folder.\n\nThey currently include:\n- `all_about_trac_5.py` (and the output in `all_about_trac_5.json`): building and executing a query that\nfetches pretty much everything related that\'s to know about the TR TRAC-5 carbine\n- `character_death_event_stream.py` (and the output in `caracter_death_event_stream.ndjson`): subscribing to\nall character death events on the SolTech server, receive at most 20 events and print them, then gracefuly\ndisconnect\n\n## Query building\n\nQueries are made on collections with optional joins on other collections as well as various\ncommands that alter the output.\n\nA Census API collection is analog to a relation in a generic relational database system, and a\nCensus API join is analog to a join between these relations.\n\n### Basic query\n\nTo build a query, instantiate the `Query` class with a `Collection` (and your service ID,\nthough it will be omitted in next examples for conciseness):\n```python\nfrom ps2_census import Collection, Query\n\nquery: Query = Query(Collection.ITEM, service_id=YOUR_SERVICE_ID)\n```\n\nChain methods to alter the query further:\n```python\nquery: Query = (\n    Query(Collection.ITEM)\n    .lang("en")\n    .sort(("item_id", 1), ("faction_id", -1))\n    .limit(30)\n    [...]\n)\n```\n\nAvailable methods are:\n- `filter(field: str, value: Union[str, int], modifier: Optional[SearchModifier])`: filter\nthe query on a field; `ps2_census.SearchModifier` contains all the modifiers made available\nby the Census API (`SearchModifier.CONTAINS`, `SearchModifier.LESS_OR_EQUAL`, ...)\n- `show(*args: str)`: only return the provided fields in results\n- `hide(*args: str)`: do not return the provided fields in results\n- `sort(*args: Tuple[str, Literal[1, -1]])`: sort the results by field, either in increasing or decreasing order\n- `has(*args: str)`: only return results which have the specified fields\n- `case(arg: bool)`: whether `filter()`s are case sensive or not; default to `True`\n- `limit(arg: int)`: limit the return to *at most* `arg` results; required in tendem with `start()` for queries having too large of a result and therefore fail; defaults to `1`\n- `limit_per_db(arg: int)`: limit the return to *at most* `arg * databases count` results; useful when\nquerying the `Collection.CHARACTER` collection whose objects are randomly distributed among multiple\ndatabases in order to have more predictable results\n- `start(arg: int)`: start with the `arg`th object within the results of the query\n- `include_null(arg: bool)`: whether to include keys with `None` values in results; defaults to `False`\n- `lang(arg: str)`: only keep the supplied language for internationalized strings\n- `join(arg: Join)`: perform a collection join; see the following documentation for additional details\n- `tree(arg: Tree)`: rearrange lists of data into trees; see the following documentation for additional details\n- `timing(arg: bool)`: show query timing information\n- `exact_match_first(arg: bool)`: when using `filter()`s with `SearchModifier`s, put exact matches at the top of the\nresults list disregarding `sort()`s\n- `distinct(arg: str)`: get the distinct values for a certain field\n- `retry(arg: bool)`: retry queries at most one time before failing; defaults to `True`\n\nExecute the query in one of the 2 ways made available by the Census API:\n\n- `.count()` to get the items count\n```python\nquery.count()\n> {\'count\': 21048}\n```\n\n- `.get()` to get the results\n```python\nquery.get()\n> {\'item_list\': [{...}, {...}, ...], \'returned\': 30}\n```\n\n`count()` and `get()` calls are when the query is actually sent to the Census API endpoint.\nThey will raise status exceptions if appropriate.\nThey also take one optional argument, `print_request_url: bool` (which defaults to `False`), that\nprints the whole request URL before raising any exceptions.\n\n### Simple join\n\nIn order to perform joins instantiate the `Join` class with a `Collection`, add any additional\nchained methods to it, and pass it to the `Query` object via `query.join()`:\n```python\nfrom ps2_census import Collection, Join, Query\n\nquery: Query = (\n    Query(Collection.ITEM)\n    .join(\n        Join(Collection.WEAPON_DATASHEET)\n        .outer(0)\n        .on("item_id")\n        .to("item_id")\n        .inject_at("weapon_datasheet")\n        [...]\n    )\n)\n```\n\nAvailable `Join` methods are:\n- `nest(other: Join)`: nest another join within this one; see the following documentation for additional details\n- `on(arg: str)`: specify the field on this collection (the `Query` or parent `Join`) to join on; if not provided will default to this collection\'s ID (`{this_type}_id`)\n- `to(arg: str)`: specify the field on the other collection (the `Join`\'s) to join to; if not provided will default to the `on` value\n- `list(arg: Literal[0, 1])`: whether the joined data is a list (and therefore will result in a list of objects) or not; `1` if it is a list, `0` if not; default to `0`\n- `show(*args: str)`: only keep the provided fields in results\n- `hide(*args: str)`: do not keep the provided fields in results\n- `inject_at(arg: str)`: the field name where the joined data will be injected in the parent element (`Query` result item or parent `Join` element)\n- `terms(**kwargs: Union[str, int])`: filter the join result by a dictionary of conditions (eg. `{"faction_id": 1, "skill_set_id": 129}`)\n- `outer(arg: Literam[0, 1])`: whether the join will perform an outer join (include non-matches) of an inner join (exclude non-matches); `1` for outer, `0` for inner; defaults to `1`\n\nMultiple joins can be performed one after another on the same `Query` and the trees will be merged in\nthe result:\n```python\nquery: Query = (\n    Query(Collection.ITEM)\n    .join(\n        Join(Collection.WEAPON_DATASHEET)\n    )\n    .join(\n        Join(Collection.ITEM_TO_WEAPON)\n    )\n)\n```\n### Nesting joins\n\n`Join`s can be nested one into another both deeply and lateraly. Nesting is done by reference: each `Join` instance\ncontains a list of references to its nested `Join`s.\n\n*Note*: be careful about modifying the `Join` instances after their creation if you assign them to locals\nfor convenience, as shown in the following examples.\nAs nesting is done by reference and the whole structure is "compiled" (into a querystring) on the call\nto `query.join()`, if any modification is done to an instance between its creation and the resolution,\nit will indirectly impact the result of any other parent and/or nested `Join`.\nMake use of the below described factories to make clean copies of your instances as necessary.\n\n#### Deeply nested join\n\nDeeply nested join are necessary in order to access data structures deeper in the collections tree.\nTo deeply nest joins, instantiate the `Join` class multiple times and combine them through\n`join1.nest(join2.nest(join3))` where `join3` is nested within `join2` and `join2` is nested within `join1`:\n```python\nfrom ps2_census import Collection, Join, Query\n\nitem_to_weapon_join: Join = (\n    Join(Collection.ITEM_TO_WEAPON)\n    .on("item_id")\n    .to("item_id")\n    .inject_at("item_to_weapon")\n)\n\nweapon_join: Join = (\n    Join(Collection.WEAPON)\n    .on("weapon_id")\n    .to("weapon_id")\n    .inject_at("weapon")\n)\n\nweapon_to_fire_group_join: Join = (\n    Join(Collection.WEAPON_TO_FIRE_GROUP)\n    .on("weapon_id")\n    .to("weapon_id")\n    .inject_at("weapon_to_fire_group")\n)\n\nquery: Query = (\n    Query(Collection.ITEM)\n    .filter("item_type_id", ItemType.WEAPON)\n    .join(\n        item_to_weapon_join.nest(\n            weapon_join.nest(\n                weapon_to_fire_group_join\n            )\n        )\n    )\n)\n```\n\n#### Lateraly nested joins\n\nLateraly nested joins are necessary in order to access data structures at the same depth in the collections tree.\nTo laterally nest joins, instantiate the `Join` class multiple times and combine them through\n`join1.nest(join2).nest(join3)` where `join2` and `join3` are nested within `join1`:\n\n```python\nfrom ps2_census import Collection, Join, Query\n\nfire_group_to_fire_mode_join: Join = (\n    Join(Collection.FIRE_GROUP_TO_FIRE_MODE)\n    .outer(0)\n    .on("fire_group_id")\n    .to("fire_group_id")\n    .list(1)\n    .inject_at("fire_group_to_fire_modes")\n)\n\nfire_mode_join: Join = (\n    Join(Collection.FIRE_MODE_2)\n    .outer(0)\n    .on("fire_mode_id")\n    .to("fire_mode_id")\n    .inject_at("fire_mode")\n)\n\nfire_mode_to_projectile_join: Join = (\n    Join(Collection.FIRE_MODE_TO_PROJECTILE)\n    .outer(0)\n    .on("fire_mode_id")\n    .to("fire_mode_id")\n    .inject_at("fire_mode_to_projectile")\n)\n\nplayer_state_group_join: Join = (\n    Join(Collection.PLAYER_STATE_GROUP_2)\n    .outer(0)\n    .on("player_state_group_id")\n    .to("player_state_group_id")\n    .list(1)\n    .inject_at("player_state_groups")\n)\n\nquery: Query = (\n    Query(Collection.WEAPON_TO_FIRE_GROUP)\n    .join(\n        fire_group_to_fire_mode_join\n        .nest(\n            fire_mode_join\n            .nest(fire_mode_to_projectile_join)\n            .nest(player_state_group_join)\n        )\n    )\n)\n```\n\n### Tree\n\nTrees are also built using their own class, `Tree`, then passed to the `Query` object:\n```python\nfrom ps2_census import Collection, Query, Tree\n\nquery: Query = (\n    Query(Collection.ITEM)\n    .tree(\n        Tree("name.en")\n        .prefix("en_name_")\n        [...]\n    )\n)\n```\n\nThis will return a dictionary of items with their english name prefixed by `en_name_` as keys\nand the objects themselves as values, instead of a flat list of items.\n\nAvailable methods are:\n- `list(arg: Literal[0, 1])`: `0` if tree data is not a list, `1` if it is a list; defaults to `0`\n- `prefix(arg: str)`: prefix to add to the field value\n- `start(arg: str)`: where the tree starts; defaults to the root (root list objects will be formatted as a tree)\n\n## Factories\n\nIn order to ease the project-wide definition of `Query`, `Join` and `Tree` objects,\neach of these classes has a `get_factory()` method.\n\nUpon invocation it saves a copy of the current object and returns a callable that, upon each invocation,\nreturns a fresh copy of that saved state.\n\n```python\nquery: Query = Query(Collection.ABILITY).has("someField").case(False)\n\nquery_factory: Callable[[], Query] = query.get_factory()\n\nquery_copy: Query = query_factory()\n\nassert query == query_copy\n> True\n\nquery = query.timing(True)\n\nassert query != query_copy\n> True\n\nquery_copy = query_copy.lang("en")\n\nanother_copy = query_factory()\n\nassert query == another_copy\n> True\n\nassert query_copy != another_copy\n> True\n```\n\nFactories obtained in this way are useful to define `Query`, `Join` or `Tree`s somewhere in your code and store\ntheir factory callables, with the guarantee that you cannot modify the shared definition anywhere.\n\n*my_module.py*\n```python\nfrom ps2_census import Query, Collection\n\nmy_query_factory: Callable[[], Query] = (\n    Query(Collection.ABILITY)\n    .lang("en")\n    .get_factory()\n)\n```\n\n*main.py*\n```python\nfrom my_module import my_query_factory\n\nquery: Query = my_query_factory()\nquery.start(100)  # does not modify any shared object\n```\n\n## Common enums\n\nCensus API data uses a lot of integer enumerations that are collections themselves.\n\nFor example the `faction_id` key in items from `Collection.ITEM` is an integer that represents\na specific fation, refering to `Collection.FACTION`: Vanu is `1`, NC is `2` etc.\n\nIn order to reduce the amount of necessary joins, which are arguably the most complex part of Census queries,\nsome common enumerations are provided in `ps2_census.enums` as Python enum.IntEnum classes, including:\n- ArmorFacing\n- FacilityType\n- Faction\n- FireModeType\n- ItemType\n- ItemCategory\n- MetagameEventState\n- PlayerState\n- ProjectileFlightType\n- ResistType\n- ResourceType\n- RewardType\n- TargetType\n- Vehicle\n- World\n\nThese typically do not change often and *ps2-census* will be updated whenever there is such a change.\n\nThey can be used just for reference, but also in queries for filtering.\n\nSee the following example for filtering weapon items only using `ps2_census.enums.ItemType`:\n```python\nfrom ps2_census.enums import ItemType\nquery = (\n    Query(Collection.ITEM)\n    .filter("item_type_id", ItemType.WEAPON)\n)\n```\n\n## Event Stream\n\n*ps2-census* offers a client that handles connection to the WebSocket endpoint, subscription\nto various streams and reception of the events.\n\n*Note*: because the client uses the [websockets](https://github.com/aaugustin/websockets) library,\nwe need to handle async calls.\n\n### Usage\n\n\nFirst you need to connect to the WebSocket endpoint; to do this, instantiate the `EventStream` class:\n```python\nfrom ps2_census import EventStream\n\nstream: EventStream = await EventStream(service_id=YOUR_SERVICE_ID)\n```\n\nThen, subscribe to events:\n```python\nfrom ps2_census import CharacterEvent, WorldEvent, EventStreamWorld, GenericCharacter\n\nawait stream.subscribe(\n    worlds=[EventStreamWorld.SOLTECH, EventStreamWorld.BRIGGS],\n    events=[CharacterEvent.DEATH, WorldEvent.CONTINENT_LOCK],\n    characters=[GenericCharacter.ALL, "1234", "5678"],\n    logical_and_characters_with_worlds=True\n)\n```\nWhere:\n- `worlds` is a list of `EventStreamWorld` objects. Use `[EventStreamWorld.ALL]` for all worlds\n- `events` is a list of `CharacterEvent`, `WorldEvent` or `GenericEvent` objects. Use `GenericEvent.ALL` to get all events (character and world)\n- `characters` is a list of character IDs as strings or the special `GenericCharacter.ALL` to subscribe to all characters\n- `logical_and_characters_with_worlds` is True if you want to match all events concerning the characters *AND* the worlds; default is False, so it matches all events concerning the characters *OR* the worlds\n\nYou can perform multiple subscriptions one after another on the same `EventStream` object; they are additively merged.\n\nFinally, you need to handle received events from your subscription:\n```python\nawait stream.receive()\n```\n\nThis simple example put together (you might want to develop it further to do more than simply print events,\nhandle graceful deconnection, etc):\n\n```python\nimport asyncio\n\nfrom ps2_census import CharacterEvent, WorldEvent, EventStream, EventStreamWorld, GenericCharacter\n\nasync def main():\n    stream: EventStream = await EventStream()\n\n    await stream.subscribe(\n        worlds=[EventStreamWorld.SOLTECH, EventStreamWorld.BRIGGS],\n        events=[CharacterEvent.DEATH, WorldEvent.CONTINENT_LOCK],\n        characters=[GenericCharacter.ALL, "1234", "5678"],\n        logical_and_characters_with_worlds=True\n    )\n\n    while True:\n        print(await stream.receive())\n\nasyncio.run(main())\n```\n\nMore information about the Planetside2 Census event stream can be found at [here](http://census.daybreakgames.com/#what-is-websocket).\n\n## Next steps\n\n- Improve the `EventStream` client to make it slightly higher-level (handle disconnections,\nfilter events, etc)\n\n## Similar projects\n\n- *leonhard-s*\' [auraxium](https://github.com/leonhard-s/auraxium)\n\n## Development\n\n### Environment\n\nIn order to develop *ps2-census*:\n- Setup a virtual environment with python 3.8\n- Install [poetry](https://github.com/python-poetry/poetry)\n- Install dependencies with `poetry install`\n- Run tests with `pytest`\n- Update dependencies with `poetry update`\n\n### Technical details\n\nAll interaction with the Census API (except the WebSocket events stream part) is done through\nquery parameters. Query bodies are always empty and all information is carried in the query URL string.\n\n*ps2-census*\' objective is to wrap the building of these (often very long and complex)\nquerystrings and provide a comfortable, structured programmatic interface.\n\nTo achieve that, we make use of the `Query` class which:\n- on initialization sets up basic query elements such as endpoint, service ID, collection and namespace,\n- at each "command" method call adds query parameters to its `parameters` dictionary attribute,\n- at `get()` or `count()` build the actual URL and execute the query using the `requests` library.\n\nCollection joins are more complicated as they are not built as proper query parameters;\ninside the final query, each call to `query.join()` creates a new query parameter, eg.\n`...&c:join={join_string}&...` with the `join_string` being written in a specific format.\n\nHence the `Join` class which:\n- on initialization sets up the collection,\n- at each method call adds items to its `items` dictionary attribute,\n- at each `nest()` call stores a reference to the nested `Join` instance in its `nested_joins` list attribute,\n- at `__str__()` builds the `join_string` that will become a main query\'s parameter value, in the specific format\nand using the `items` elements as well as recursively calling `__str__()` on `nested_joins` items.\n\nTherefore, the `join_string` is not actually computed before a call to a `Join` instance\'s `__str__()` method,\nand it is recursively computed across nested joins.\n\nThe event streams part is more conventional, as *ps2-census* just needs to connect to a WebSocket endpoint, issue\nsubscription commands in a custom but simple format and then read messages from the stream.\n',
    'author': 'Sylvain Pascou',
    'author_email': 'sylvain@pascou.net',
    'maintainer': None,
    'maintainer_email': None,
    'url': 'https://github.com/spascou/ps2-census',
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
    'python_requires': '>=3.8,<4.0',
}


setup(**setup_kwargs)
