Skip to content
Open
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
213 changes: 213 additions & 0 deletions docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
from dataclasses import dataclass
from enum import Enum
from inspect import _empty
from typing import Any, Optional

from fuzzly import Client, FuzzlyClient


client: FuzzlyClient = FuzzlyClient()
c: Client = Client()

funcs: list[str] = [i for i in client.__dir__() if not i.startswith('_') and i not in set(c.__dir__())]
models: list[type] = []

bullet = "\n- "

client_header = """[See the home page for client setup](.)

# Client Methods
Notice that all client functions are `async`. This is because http requests are made under the hood and they are done in parallel with other requests."""
models_header = """[See the client page for how to retrieve and use these models](./Client)"""


def docstrip(doc: str) -> str :
doc = doc.strip('\n\r')
indent = len(doc) - len(doc.lstrip('\t'))
return '\n'.join(docstr.removeprefix('\t' * indent) for docstr in doc.split('\n')).strip()


def model_name(model: type) -> str :
module = model.__module__

if module.endswith('._shared') :
module = module[:-8]

return f'{module}.{model.__name__}'


def model_link(model: type, local: bool = False) -> str :
if local :
return '#' + model_name(model).replace('.', '').lower()
return './Models#' + model_name(model).replace('.', '').lower()


def valuestr(value: Any) -> str :
for cls in getattr(value.__class__, '__mro__', []) :
if cls == Enum :
return f'{value.__class__.__name__}.{value.name}'

if cls in { int, float, str } :
return str(value)

raise ValueError(f'no known conversion for {value.__class__}')


def modelstr(model: type, link: bool = False) -> str :
m = model
fmt = '{}'
name = m.__name__

if hasattr(model, '__args__') :
m = model.__args__[0]
fmt = f'{model.__name__}[{{}}]'
name = modelstr(m)

if link :
return fmt.format(f'<a href="{model_link(m)}">{name}</a>')

else :
return fmt.format(name)


@dataclass
class Param :
name: str
type: str
default: Optional[str] = None

def doc(self) -> str :
doc = f'{self.name}: '

if 'fuzzly' in self.type.__module__ :
models.append(self.type)

doc += modelstr(self.type, link=True)
doc = f'<code>{doc}</code>'

else :
doc += modelstr(self.type)
doc = f'`{doc}`'

if self.default :
doc += f' (optional, default: `{valuestr(self.default)}`)'

return doc

def __str__(self) -> str :
if self.default :
return f'{self.name}: {modelstr(self.type)} = {valuestr(self.default)}'

else :
return f'{self.name}: {modelstr(self.type)}'


def funcdoc(funcstr: str) -> str :
func = getattr(client, funcstr)
docstr = docstrip(func.__doc__)

params: list[Param] = []
__omit__ = { 'self', 'auth' }
for key, param in func.__signature__.parameters.items() :
if key in __omit__ :
continue

p = Param(name=param.name, type=param.annotation)
if param.default != _empty :
p.default = param.default

params.append(p)

r = func.__signature__.return_annotation
if 'fuzzly' in r.__module__ :
models.append(r)
if hasattr(r, '__args__') :
for m in r.__args__ :
if 'fuzzly' in m.__module__ :
models.append(m)

title = f'({func.__self__.__class__.__name__}).{func.__name__}({", ".join([i.name for i in params])}) -> {modelstr(r)}'

doc = f'## {title}\n{docstr}'

if params :
doc += f'\n\n#### params{bullet}{bullet.join(map(Param.doc, params))}'

return f'{doc}\n\n#### returns\n- <code>{modelstr(r, link=True)}</code>'


def model_subtypes(model: type) -> list[type] :
subtypes: list[type] = []

if 'fuzzly' in model.__module__ :
subtypes.append(model)

if hasattr(model, '__args__') :
for m in model.__args__ :
subtypes += model_subtypes(m)

return subtypes


def modeldoc(model: type) -> str :
doc = ""
if model.__doc__ :
doc = docstrip(model.__doc__)

classdef = f'```python\nclass {model.__name__}'
if len(model.__mro__) > 2 :
classdef += f'({model.__mro__[1].__name__})'
classdef += ':'

subtypes: list[type] = []

if hasattr(model, 'model_fields') :
if doc :
doc += '\n\n'
doc += classdef

for name, field in model.model_fields.items() :
s = model_subtypes(field.annotation)
models.extend(s)
subtypes.extend(s)

doc += f'\n\t{name}: {modelstr(field.annotation)}'
doc += '\n```'

if issubclass(model, Enum) :
if doc :
doc += '\n\n'
doc += classdef

for member in model.__members__.values() :
doc += f'\n\t{member.name}: {type(member.value).__name__} = \'{member.value}\''

doc += '\n```'

_completed_subtypes: set[type] = set()
if subtypes :
doc += '\n\n#### subtypes'
for subtype in subtypes :
if subtype in _completed_subtypes :
continue

doc += f'{bullet}[`{subtype.__name__}`]({model_link(subtype, local=True)})'
_completed_subtypes.add(subtype)

return f'### `{model_name(model)}`\n{doc}'


with open('client.md', 'w') as file :
file.write(client_header + '\n\n\n' + '\n\n\n'.join(map(funcdoc, funcs)))

with open('models.md', 'w') as file :
doc = models_header
_completed_models: set[type] = set()
for model in models :
if model in _completed_models :
continue

doc += '\n\n\n' + modeldoc(model)
_completed_models.add(model)

file.write(doc)
79 changes: 78 additions & 1 deletion fuzzly/__init__.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,112 @@
"""A python client library for fuzz.ly""" # this comment should match the package slogan under the logo in the readme


__version__: str = '0.0.4'
__version__: str = '0.0.5'


from typing import List

from .api.post import FetchMyPosts, FetchPost
from .api.set import FetchPostSets, FetchSet, FetchUserSets
from .api.tag import FetchPostTags, FetchTag
from .client import Client
from .models.post import Post, PostId, PostSort
from .models.set import PostSet, Set, SetId
from .models.tag import Tag, TagGroups


class FuzzlyClient(Client) :
"""
Notice that all client functions are `async`. This is because http requests are made under the hood and they are done in parallel with other requests.
"""

################################################## POST ##################################################

@Client.authenticated
async def post(self: Client, post_id: PostId, auth: str = None) -> Post :
"""
Retrieves the post indicated by the provided `post_id`.
```python
client: FuzzlyClient
await client.post('Lw_KpQM6')
```
"""
return await FetchPost(post_id=post_id, auth=auth)


@Client.authenticated
async def my_posts(self: Client, sort: PostSort = PostSort.new, count: int = 64, page: int = 1, auth: str = None) -> List[Post] :
"""
Retrieves the user's own posts. Requires a bot token to be provided to the client.
```python
client: FuzzlyClient
await client.my_posts()
```
"""
return await FetchMyPosts({ 'sort': sort.name, 'count': count, 'page': page }, auth=auth)


################################################## TAGS ##################################################

@Client.authenticated
async def tag(self: Client, tag: str, auth: str = None) -> Tag :
"""
Retrieves the tag specified by the provided `tag`.
```python
client: FuzzlyClient
await client.tag('female')
```
"""
return await FetchTag(tag=tag, auth=auth)


@Client.authenticated
async def post_tags(self: Client, post_id: PostId, auth: str = None) -> TagGroups :
"""
Retrieves the tags belonging to a post, specified by the provided `post_id`.
```python
client: FuzzlyClient
await client.post_tags('Lw_KpQM6')
```
"""
return await FetchPostTags(post_id=post_id, auth=auth)


################################################## SETS ##################################################

@Client.authenticated
async def set(self: Client, set_id: SetId, auth: str = None) -> Set :
"""
Retrieves the set indicated by the provided `set_id`.
```python
client: FuzzlyClient
await client.set('abc-123')
```
"""
return await FetchSet(set_id=set_id, auth=auth)


@Client.authenticated
async def user_sets(self: Client, handle: str, auth: str = None) -> List[Set] :
"""
Retrieves the sets owned by the user indicated by the provided `handle`.
```python
client: FuzzlyClient
await client.user_sets('handle')
```
"""
return await FetchUserSets(handle=handle, auth=auth)


@Client.authenticated
async def post_sets(self: Client, post_id: PostId, auth: str = None) -> List[PostSet] :
"""
Retrieves all sets that contain the post indicated by the provided `post_id`.

NOTE: the return model also contains the post's neighbors within the set
```python
client: FuzzlyClient
await client.post_sets('abcd1234')
```
"""
return await FetchPostSets(post_id=post_id, auth=auth)
18 changes: 18 additions & 0 deletions fuzzly/api/set.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import List

from kh_common.gateway import Gateway

from ..constants import SetHost
from ..models.set import PostSet, Set


# Usage: FetchSet(set_id='abcd123')
FetchSet = Gateway(SetHost + '/v1/set/{set_id}', Set, method='GET')


# Usage: FetchUserSets(handle='@handle')
FetchUserSets = Gateway(SetHost + '/v1/user/{handle}', List[Set], method='GET')


# Usage: FetchPostSets(post_id='abcd1234')
FetchPostSets = Gateway(SetHost + '/v1/post/{post_id}', List[PostSet], method='GET')
4 changes: 2 additions & 2 deletions fuzzly/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def error_handler(func: Callable) -> Callable :
Transforms aiohttp.ClientResponseError back to their original kh_common.exceptions.http_error.HttpError instance for re-raising and/or handling internally.

Usage
```
```python
class MyClient(Client) :
@Client.authenticated
async test(auth: str = None) -> str :
Expand Down Expand Up @@ -107,7 +107,7 @@ def authenticated(func: Callable) -> Callable :
Injects an authenticated bot token into the 'auth' kwarg of the passed function

Usage
```
```python
class MyClient(Client) :
@Client.authenticated
async test(auth: str = None) -> str :
Expand Down
Loading