The Procedure Index#

All the possible API calls can be found under client.p. Because there are so many methods, they have been grouped under sub-objects, organised by resource kind.

>>> import redditwarp.SYNC
>>> client = redditwarp.SYNC.Client()
>>> client.p.
client.p.account              client.p.misc
client.p.collection           client.p.moderation
client.p.comment              client.p.modmail
client.p.comment_tree         client.p.ping()
client.p.custom_feed          client.p.submission
client.p.draft                client.p.subreddit
client.p.flair                client.p.subreddit_style_new
client.p.flair_emoji          client.p.subreddit_style_old
client.p.front                client.p.user
client.p.live_thread          client.p.widget
client.p.message              client.p.wiki
>>> client.p.

Discovering procedures#

The organisation of the methods by resource kind makes it easy to locate API procedures.

For example, let’s say we want to post a submission to a subreddit. Since this is a submission operation, the procedure will be found somewhere under client.p.submission.

While actively coding, IDEs will greatly help in navigating the procedure index. If you’re not using an IDE, we could write client.p.submission.<TAB> in an interactive Python REPL terminal session to see all the submission-related procedures.

>>> client.p.submission.
client.p.submission.MediaUploading(               client.p.submission.media_uploading(
client.p.submission.apply_removal_reason(         client.p.submission.pin_to_profile(
client.p.submission.approve(                      client.p.submission.remove(
client.p.submission.bulk_fetch(                   client.p.submission.remove_spam(
client.p.submission.bulk_hide(                    client.p.submission.reply(
client.p.submission.bulk_unhide(                  client.p.submission.save(
client.p.submission.create_crosspost(             client.p.submission.search(
client.p.submission.create_gallery_post(          client.p.submission.send_removal_comment(
client.p.submission.create_image_post(            client.p.submission.send_removal_message(
client.p.submission.create_link_post(             client.p.submission.set_contest_mode(
client.p.submission.create_poll_post(             client.p.submission.set_event_time(
client.p.submission.create_text_post(             client.p.submission.set_suggested_sort(
client.p.submission.create_video_post(            client.p.submission.snooze_reports(
client.p.submission.delete(                       client.p.submission.sticky(
client.p.submission.disable_reply_notifications(  client.p.submission.undistinguish(
client.p.submission.distinguish(                  client.p.submission.unfollow_event(
client.p.submission.duplicates(                   client.p.submission.unhide(
client.p.submission.edit_text_post_body(          client.p.submission.unignore_reports(
client.p.submission.enable_reply_notifications(   client.p.submission.unlock(
client.p.submission.fetch(                        client.p.submission.unmark_nsfw(
client.p.submission.follow_event(                 client.p.submission.unmark_spoiler(
client.p.submission.get(                          client.p.submission.unpin_from_profile(
client.p.submission.hide(                         client.p.submission.unsave(
client.p.submission.ignore_reports(               client.p.submission.unsnooze_reports(
client.p.submission.lock(                         client.p.submission.unsticky(
client.p.submission.mark_nsfw(                    client.p.submission.vote(
client.p.submission.mark_spoiler(
>>> client.p.submission.

Most resource kind sub-object groups will have methods get() and fetch(). These methods are usually used to retrieve information about a specific resource by its ID.

subm = client.p.submission.fetch('cqufij')
print(subm.author_display_name)
print(subm.title)
print(subm.created_at)
print(subm.permalink)

Get vs. fetch#

The get() and fetch() methods are used to retrieve a resource by ID. These procedures are shared by the majority of resource kinds in the procedure index. The difference between them is that fetch() raises an exception when it fails to retrieve a resource, whereas get() will return None.

The exception type thrown by fetch() is not sugar-coated and may vary based on the behaviour of the underlying endpoint. In some cases, the API does not produce an error when a retrieval fails and instead returns no information. RedditWarp will force an exception to occur in these cases by raising a synthetic redditwarp.exceptions.NoResultException exception.

>>> client.p.user.fetch_by_name('sdfaxzzdfv')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/danpro/Desktop/redditwarp/redditwarp/siteprocs/user/SYNC.py", line 108, in fetch_by_name
    root = self._client.request('GET', f'/user/{name}/about')
  File "/Users/danpro/Desktop/redditwarp/redditwarp/client_SYNC.py", line 226, in request
    resp.ensure_successful_status()
  File "/Users/danpro/Desktop/redditwarp/redditwarp/http/response.py", line 31, in ensure_successful_status
    ensure_successful_status(self.status)
  File "/Users/danpro/Desktop/redditwarp/redditwarp/http/exceptions.py", line 124, in ensure_successful_status
    raise_now(n)
  File "/Users/danpro/Desktop/redditwarp/redditwarp/http/exceptions.py", line 119, in raise_now
    raise get_status_code_exception_class_by_status_code(n)(status_code=n)
redditwarp.http.exceptions.StatusCodeExceptionTypes.NotFound: 404 Not Found
>>> client.p.submission.fetch(999)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/danpro/Desktop/redditwarp/redditwarp/siteprocs/submission/fetch_SYNC.py", line 22, in __call__
    return self.by_id36(id36)
  File "/Users/danpro/Desktop/redditwarp/redditwarp/siteprocs/submission/fetch_SYNC.py", line 32, in by_id36
    raise NoResultException('target not found')
redditwarp.exceptions.NoResultException: target not found

When using the get() method, it’s important to explicitly check if the returned object is None to maintain the type-safety of your program. Your IDE or type-checker should notify you if you forget to do this.

In general, prefer the fetch() method when you expect the resource to exist, and get() when you don’t care as much about whether the resource exists.

Exceptions#

View the docstring of a procedure index method to learn about the possible exceptions it could raise. Note that the exceptions listed may not be exhaustive; for example, if Reddit servers are down, many API requests could result in a 500 HTTP status code exception.

The redditwarp.http.exceptions.StatusCodeException exception is a typical exception type that occurs from API procedures. Instances of this exception have a status_code attribute.

try:
    user = client.p.user.fetch_by_name('sdfaxzzdfv')
except redditwarp.http.exceptions.StatusCodeException as e:
    if e.status_code == 404:
        print('User not found')
    else:
        print('Unknown error occurred')
        raise
else:
    print(user.total_karma)

The status code exception gets raised as a last resort when more detailed error information cannot be found in the response data. Most API errors are reported in the form of a redditwarp.exceptions.RedditError exception.

There are three string fields on RedditError: label, explanation, and field. The label field will always be filled out, while the other two could be empty strings, although the explanation field is usually not empty, unlike field.

When catching the RedditError exception, it is useful to check against label.

try:
    client.p.message.send(
            'TheSantaManta', "Evil Christmas wish",
            "Dear SantaManta, I want the world for Christmas.")
except redditwarp.exceptions.RedditError as e:
    if e.label == 'USER_DOESNT_EXIST':
        print('The user does not exist')
    else:
        raise

If we didn’t catch the exception, this is what the exception traceback would look like.

Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "/Users/danpro/Desktop/redditwarp/redditwarp/siteprocs/message/SYNC.py", line 31, in send
    self._client.request('POST', '/api/compose', data=req_data)
  File "/Users/danpro/Desktop/redditwarp/redditwarp/client_SYNC.py", line 236, in request
    snub(json_data)
  File "/Users/danpro/Desktop/redditwarp/redditwarp/exceptions.py", line 177, in raise_for_reddit_error
    raise RedditError(label=label, explanation=explanation, field=field)
redditwarp.exceptions.RedditError: USER_DOESNT_EXIST: "that user doesn't exist" -> to

Bulk retrieval#

Bulk operations are composed of batched API requests. Bulk operation procedures typically return an iterator object. This iterator object should be consumed to evaluate all the batched network requests.

If the iterator doesn’t return anything useful, an empty for loop will do the trick. E.g.:

it: Iterable[int] = [...]
itr = client.p.modmail.conversation.bulk_mark_read(it)
for _ in itr:
    pass

Network errors are possible during iteration, and an exception can be thrown on the for loop line. The iterator, however, won’t break if this happens and it can be reentered if needed.

itr = client.p.submission.bulk_fetch([...])
while True:
    try:
        for item in itr:
            process_item(item)
    except Exception:
        time.sleep(60)
        continue
    break

The downside of this approach is that the try block may be covering too much, since it’s not possible in Python syntax to put a try..except around only the for loop line and not its body. For more precise control over error handling, it is preferable to evaluate call chunks directly.

Use get_chunking_iterator() to access the underlying call chunks:

itr = client.p.submission.bulk_fetch([...])
chunks = itr.get_chunking_iterator()
for chunk in chunks:
    while True:
        try:
            results = chunk()
        except Exception:
            time.sleep(60)
            continue
        break
    for item in results:
        process_item(item)

There are actually two types of iterators that are returned by bulk_* methods: a CallChunkCallingIterator and a CallChunkChainingIterator. The former returns single objects (often None), while the latter returns a sequence of objects when its call chunks are evaluated.

Both CallChunkCallingIterator and CallChunkChainingIterator objects have a .current_callable property that will be assigned a callable object if a call chunk call fails during iteration, otherwise it is None.

The CallChunkChainingIterator object also has a .current_iterator property which contains an iterator that may be populated if the main iterator was interrupted.

itr: CallChunkChainingIterator[Submission] = client.p.submission.bulk_fetch([...])
try:
    for item in itr:
        process_item(item)
except Exception:
    pass

# The `itr.current_iterator` object will be a non-empty iterator
# if an exception was caused by the `process_item(item)` line above.
for item in itr.current_iterator:
    process_item(item)

# The `itr.current_callable` attribute will be non-`None`
# if an exception was caused by the `for item in itr:` line above.
if itr.current_callable is not None:
    for item in itr.current_callable():
        process_item(item)
    itr.current_callable = None

Last thing to note, the iterator modules don’t have names with SYNC/ASYNC. The reason for this is because an async iterator can be made to accept sync or async iterable input, so there could theoretically be two versions of an async iterator. Additionally, it can occasionally make sense to use a sync iterator in an async program.

# SYNC
from redditwarp.iterators.call_chunk_chaining_iterator import CallChunkChainingIterator
# ASYNC
from redditwarp.iterators.call_chunk_chaining_async_iterator import CallChunkChainingAsyncIterator