Skip to content

User

A representation of a User on Wattpad. Note: Users are singletons, unique as per their username. Two user classes with the same username are the same.

Attributes:

Name Type Description
username str

Lowercased username of this User.

stories list[Story]

Stories authored by this User.

followers list[User]

Users that follow this User.

following list[User]

Users this User follows.

lists set[List]

Lists created by this User.

data UserModel

User Data from the Wattpad API.

Source code in src/wattpad/wattpad.py
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
class User(metaclass=create_singleton()):
    """A representation of a User on Wattpad.
    **Note**: Users are singletons, unique as per their username. Two user classes with the same username are the _same_.

    Attributes:
        username (str): Lowercased username of this User.
        stories (list[Story]): Stories authored by this User.
        followers (list[User]): Users that follow this User.
        following (list[User]): Users this User follows.
        lists (set[List]): Lists created by this User.
        data (UserModel): User Data from the Wattpad API.
    """

    def __init__(self, username: str, **kwargs):
        """Create a User object.

        Args:
            username (str): The username of this User.
            **kwargs (any): Arguments to pass directly to the underlying `UserModel`. These are ignored if the User has been instantiated earlier in the runtime.
        """
        self.username = username.lower()
        self.stories: list[Story] = []
        self.followers: set[User] = set()
        self.following: set[User] = set()
        self.lists: set[List] = set()

        self.data = UserModel(username=self.username, **kwargs)

    def __repr__(self) -> str:
        return f"<User username={self.username}>"

    async def fetch(self, include: bool | UserModelFieldsType = False) -> dict:
        """Populates a User's data. Call this method after instantiation.

        Args:
            include (bool | UserModelFieldsType, optional): Fields to fetch. True fetches all fields. Defaults to False.

        Returns:
            dict: The raw API Response.
        """
        if include is False:
            include_fields: UserModelFieldsType = {}
        elif include is True:
            include_fields: UserModelFieldsType = {
                key: True for key in get_fields(UserModel)  # type: ignore
            }
        else:
            include_fields: UserModelFieldsType = include

        data = cast(
            dict,
            await fetch_url(
                build_url(f"users/{self.data.username}", fields=dict(include_fields))
            ),
        )
        if "username" in data:
            data.pop("username")

        self.data = UserModel(username=self.username, **data)

        return data

    async def fetch_stories(self, include: bool | StoryModelFieldsType = False) -> dict:
        """Fetch a User's authored stories.

        Args:
            include (bool | StoryModelFieldsType, optional): Fields of authored stories to fetch. True fetches all fields. Defaults to False.

        Returns:
            dict: The raw API Response.
        """
        if include is False:
            include_fields: StoryModelFieldsType = {}
        elif include is True:
            include_fields: StoryModelFieldsType = {
                key: True for key in get_fields(StoryModel)  # type: ignore
            }
        else:
            include_fields: StoryModelFieldsType = include

        include_fields["id"] = True

        if "tagRankings" in include_fields:
            if type(include_fields["tagRankings"]) is dict:
                include_fields["tagRankings"]["name"] = True

        if "parts" in include_fields:
            if type(include_fields["parts"]) is dict:
                include_fields["parts"]["id"] = True

        url = (
            build_url(f"users/{self.data.username}/stories", fields=None)
            + f"&fields=stories({construct_fields(dict(include_fields))})"  # ! The field format for story retrieval differs here. It's /stories?fields=stories(<fields>). Compared to the usual /path?fields=<fields>. For this reason, we need to manually edit the fields in.
        )
        data = cast(dict, await fetch_url(url))

        stories: list[Story] = []
        for story in data["stories"]:
            if "user" in story:
                story.pop("user")

            id_ = story.pop("id")

            story_cls = Story(
                user=self, id=id_
            )  # ! This code is an artefact of the singleton design model. If a user already exists, their data will not be updated otherwise.
            story_cls._update_data(**story)

            stories.append(story_cls)

        self.stories = stories
        self.data.num_stories_published = len(
            self.stories
        )  # ! The data['total'] can also be used, but it isn't always present. (Based on included_fields.)

        return data

    async def fetch_followers(
        self,
        include: bool | UserModelFieldsType = False,
        limit: Optional[int] = None,
        offset: Optional[int] = None,
    ) -> dict:
        """Fetches the User's followers.

        Args:
            include (bool | UserModelFieldsType, optional): Fields of the following users' to fetch. True fetches all fields. Defaults to False.
            limit (Optional[int], optional): Maximum number of users to return at once. Use this alongside `offset` for better performance. Defaults to None.
            offset (Optional[int], optional): Number of users to skip before returning followers. Use this alongside `limit` for better performance. Defaults to None.

        Returns:
            dict: The raw API Response.
        """
        if include is False:
            include_fields: UserModelFieldsType = {}
        elif include is True:
            include_fields: UserModelFieldsType = {
                key: True for key in get_fields(UserModel)  # type: ignore
            }
        else:
            include_fields: UserModelFieldsType = include

        include_fields["username"] = True

        url = (
            build_url(
                f"users/{self.data.username}/followers",
                fields=None,
                limit=limit,
                offset=offset,
            )
            + f"&fields=users({construct_fields(dict(include_fields))})"  # ! Similar to story retrieval, requested fields need to be wrapped in `users(<fields>)`.
        )
        data = cast(dict, await fetch_url(url))

        followers: set[User] = set()
        for user in data["users"]:
            username = user.pop("username")
            user_cls = User(
                username=username
            )  # ! This code is an artefact of the singleton design model. If a user already exists, their data will not be updated otherwise.
            user_cls.following.add(
                self
            )  # ! The current user is followed by this fetched user
            user_cls._update_data(**user)
            followers.add(user_cls)

        self.followers.update(followers)
        self.data.num_followers = len(self.followers)

        return data

    async def fetch_following(
        self,
        include: bool | UserModelFieldsType = False,
        limit: Optional[int] = None,
        offset: Optional[int] = None,
    ) -> dict:
        """Fetch the users this User follows.

        Args:
            include (bool | UserModelFieldsType, optional): Fields of the followed users' to fetch. True fetches all fields. Defaults to False.
            limit (Optional[int], optional): Maximum number of users to return at once. Use this alongside `offset` for better performance. Defaults to None.
            offset (Optional[int], optional): Number of users to skip before returning followers. Use this alongside `limit` for better performance. Defaults to None.

        Returns:
            dict: The raw API Response.
        """
        if include is False:
            include_fields: UserModelFieldsType = {}
        elif include is True:
            include_fields: UserModelFieldsType = {
                key: True for key in get_fields(UserModel)  # type: ignore
            }
        else:
            include_fields: UserModelFieldsType = include

        include_fields["username"] = True

        url = (
            build_url(
                f"users/{self.data.username}/following",
                fields=None,
                limit=limit,
                offset=offset,
            )
            + f"&fields=users({construct_fields(dict(include_fields))})"  # ! Similar to story retrieval, requested fields need to be wrapped in `users(<fields>)`.
        )
        data = cast(dict, await fetch_url(url))

        following: set[User] = set()
        for user in data["users"]:
            username = user.pop("username")

            user_cls = User(
                username=username
            )  # ! This code is an artefact of the singleton design model. If a user already exists, their data will not be updated otherwise.
            user_cls.followers.add(self)  # ! The current user follows this fetched user
            user_cls._update_data(**user)

            following.add(user_cls)

        self.followers.update(following)
        self.data.num_following = len(self.following)

        return data

    async def fetch_lists(
        self,
        include: bool | ListModelFieldsType = False,
        limit: Optional[int] = None,
        offset: Optional[int] = None,
    ) -> dict:
        """Fetch a User's lists.

        Args:
            include (bool | ListModelFieldsType, optional): Fields of the lists to fetch. True fetches all fields. Defaults to False.
            limit (Optional[int], optional): Maximum number of users to return at once. Use this alongside `offset` for better performance. Defaults to None.
            offset (Optional[int], optional): Number of users to skip before returning followers. Use this alongside `limit` for better performance. Defaults to None.

        Returns:
            dict: The raw API Response.
        """
        if include is False:
            include_fields: ListModelFieldsType = {}
        elif include is True:
            include_fields: ListModelFieldsType = {
                key: True for key in get_fields(ListModel)  # type: ignore
            }
        else:
            include_fields: ListModelFieldsType = include

        include_fields["id"] = True

        if "stories" in include_fields:
            if type(include_fields["stories"]) is dict:
                include_fields["stories"]["id"] = True

        url = (
            build_url(
                f"users/{self.data.username}/lists",
                fields=None,
                limit=limit,
                offset=offset,
            )
            + f"&fields=lists({construct_fields(dict(include_fields))})"  # ! Similar to story retrieval, requested fields need to be wrapped in `lists(<fields>)`.
        )
        data = cast(dict, await fetch_url(url))

        lists: set[List] = set()
        for list_ in data["lists"]:
            id_ = list_.pop("id")
            list_cls = List(
                id=id_, user=self
            )  # ! This code is an artefact of the singleton design model. If a list already exists, its data will not be updated otherwise.

            stories: set[Story] = set()
            for story in list_["stories"]:
                s_id_ = story.pop("id")
                story_cls = Story(id=s_id_)
                story_cls._update_data(**story)
                stories.add(story_cls)

            list_cls.stories.update(stories)
            lists.add(list_cls)

        self.lists.update(lists)
        self.data.num_lists = len(self.lists)

        return data

    def _update_data(self, **kwargs):
        """Updates self.data with kwargs, overwriting any duplicate values with a preference towards kwargs.

        Returns:
            None: Nothing is returned.
        """
        self.data = self.data.model_copy(
            update=convert_from_aliases(kwargs, self.data), deep=True
        )

__init__(username, **kwargs) #

Create a User object.

Parameters:

Name Type Description Default
username str

The username of this User.

required
**kwargs any

Arguments to pass directly to the underlying UserModel. These are ignored if the User has been instantiated earlier in the runtime.

{}
Source code in src/wattpad/wattpad.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def __init__(self, username: str, **kwargs):
    """Create a User object.

    Args:
        username (str): The username of this User.
        **kwargs (any): Arguments to pass directly to the underlying `UserModel`. These are ignored if the User has been instantiated earlier in the runtime.
    """
    self.username = username.lower()
    self.stories: list[Story] = []
    self.followers: set[User] = set()
    self.following: set[User] = set()
    self.lists: set[List] = set()

    self.data = UserModel(username=self.username, **kwargs)

fetch(include=False) async #

Populates a User's data. Call this method after instantiation.

Parameters:

Name Type Description Default
include bool | UserModelFieldsType

Fields to fetch. True fetches all fields. Defaults to False.

False

Returns:

Name Type Description
dict dict

The raw API Response.

Source code in src/wattpad/wattpad.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
async def fetch(self, include: bool | UserModelFieldsType = False) -> dict:
    """Populates a User's data. Call this method after instantiation.

    Args:
        include (bool | UserModelFieldsType, optional): Fields to fetch. True fetches all fields. Defaults to False.

    Returns:
        dict: The raw API Response.
    """
    if include is False:
        include_fields: UserModelFieldsType = {}
    elif include is True:
        include_fields: UserModelFieldsType = {
            key: True for key in get_fields(UserModel)  # type: ignore
        }
    else:
        include_fields: UserModelFieldsType = include

    data = cast(
        dict,
        await fetch_url(
            build_url(f"users/{self.data.username}", fields=dict(include_fields))
        ),
    )
    if "username" in data:
        data.pop("username")

    self.data = UserModel(username=self.username, **data)

    return data

fetch_followers(include=False, limit=None, offset=None) async #

Fetches the User's followers.

Parameters:

Name Type Description Default
include bool | UserModelFieldsType

Fields of the following users' to fetch. True fetches all fields. Defaults to False.

False
limit Optional[int]

Maximum number of users to return at once. Use this alongside offset for better performance. Defaults to None.

None
offset Optional[int]

Number of users to skip before returning followers. Use this alongside limit for better performance. Defaults to None.

None

Returns:

Name Type Description
dict dict

The raw API Response.

Source code in src/wattpad/wattpad.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
async def fetch_followers(
    self,
    include: bool | UserModelFieldsType = False,
    limit: Optional[int] = None,
    offset: Optional[int] = None,
) -> dict:
    """Fetches the User's followers.

    Args:
        include (bool | UserModelFieldsType, optional): Fields of the following users' to fetch. True fetches all fields. Defaults to False.
        limit (Optional[int], optional): Maximum number of users to return at once. Use this alongside `offset` for better performance. Defaults to None.
        offset (Optional[int], optional): Number of users to skip before returning followers. Use this alongside `limit` for better performance. Defaults to None.

    Returns:
        dict: The raw API Response.
    """
    if include is False:
        include_fields: UserModelFieldsType = {}
    elif include is True:
        include_fields: UserModelFieldsType = {
            key: True for key in get_fields(UserModel)  # type: ignore
        }
    else:
        include_fields: UserModelFieldsType = include

    include_fields["username"] = True

    url = (
        build_url(
            f"users/{self.data.username}/followers",
            fields=None,
            limit=limit,
            offset=offset,
        )
        + f"&fields=users({construct_fields(dict(include_fields))})"  # ! Similar to story retrieval, requested fields need to be wrapped in `users(<fields>)`.
    )
    data = cast(dict, await fetch_url(url))

    followers: set[User] = set()
    for user in data["users"]:
        username = user.pop("username")
        user_cls = User(
            username=username
        )  # ! This code is an artefact of the singleton design model. If a user already exists, their data will not be updated otherwise.
        user_cls.following.add(
            self
        )  # ! The current user is followed by this fetched user
        user_cls._update_data(**user)
        followers.add(user_cls)

    self.followers.update(followers)
    self.data.num_followers = len(self.followers)

    return data

fetch_following(include=False, limit=None, offset=None) async #

Fetch the users this User follows.

Parameters:

Name Type Description Default
include bool | UserModelFieldsType

Fields of the followed users' to fetch. True fetches all fields. Defaults to False.

False
limit Optional[int]

Maximum number of users to return at once. Use this alongside offset for better performance. Defaults to None.

None
offset Optional[int]

Number of users to skip before returning followers. Use this alongside limit for better performance. Defaults to None.

None

Returns:

Name Type Description
dict dict

The raw API Response.

Source code in src/wattpad/wattpad.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
async def fetch_following(
    self,
    include: bool | UserModelFieldsType = False,
    limit: Optional[int] = None,
    offset: Optional[int] = None,
) -> dict:
    """Fetch the users this User follows.

    Args:
        include (bool | UserModelFieldsType, optional): Fields of the followed users' to fetch. True fetches all fields. Defaults to False.
        limit (Optional[int], optional): Maximum number of users to return at once. Use this alongside `offset` for better performance. Defaults to None.
        offset (Optional[int], optional): Number of users to skip before returning followers. Use this alongside `limit` for better performance. Defaults to None.

    Returns:
        dict: The raw API Response.
    """
    if include is False:
        include_fields: UserModelFieldsType = {}
    elif include is True:
        include_fields: UserModelFieldsType = {
            key: True for key in get_fields(UserModel)  # type: ignore
        }
    else:
        include_fields: UserModelFieldsType = include

    include_fields["username"] = True

    url = (
        build_url(
            f"users/{self.data.username}/following",
            fields=None,
            limit=limit,
            offset=offset,
        )
        + f"&fields=users({construct_fields(dict(include_fields))})"  # ! Similar to story retrieval, requested fields need to be wrapped in `users(<fields>)`.
    )
    data = cast(dict, await fetch_url(url))

    following: set[User] = set()
    for user in data["users"]:
        username = user.pop("username")

        user_cls = User(
            username=username
        )  # ! This code is an artefact of the singleton design model. If a user already exists, their data will not be updated otherwise.
        user_cls.followers.add(self)  # ! The current user follows this fetched user
        user_cls._update_data(**user)

        following.add(user_cls)

    self.followers.update(following)
    self.data.num_following = len(self.following)

    return data

fetch_lists(include=False, limit=None, offset=None) async #

Fetch a User's lists.

Parameters:

Name Type Description Default
include bool | ListModelFieldsType

Fields of the lists to fetch. True fetches all fields. Defaults to False.

False
limit Optional[int]

Maximum number of users to return at once. Use this alongside offset for better performance. Defaults to None.

None
offset Optional[int]

Number of users to skip before returning followers. Use this alongside limit for better performance. Defaults to None.

None

Returns:

Name Type Description
dict dict

The raw API Response.

Source code in src/wattpad/wattpad.py
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
async def fetch_lists(
    self,
    include: bool | ListModelFieldsType = False,
    limit: Optional[int] = None,
    offset: Optional[int] = None,
) -> dict:
    """Fetch a User's lists.

    Args:
        include (bool | ListModelFieldsType, optional): Fields of the lists to fetch. True fetches all fields. Defaults to False.
        limit (Optional[int], optional): Maximum number of users to return at once. Use this alongside `offset` for better performance. Defaults to None.
        offset (Optional[int], optional): Number of users to skip before returning followers. Use this alongside `limit` for better performance. Defaults to None.

    Returns:
        dict: The raw API Response.
    """
    if include is False:
        include_fields: ListModelFieldsType = {}
    elif include is True:
        include_fields: ListModelFieldsType = {
            key: True for key in get_fields(ListModel)  # type: ignore
        }
    else:
        include_fields: ListModelFieldsType = include

    include_fields["id"] = True

    if "stories" in include_fields:
        if type(include_fields["stories"]) is dict:
            include_fields["stories"]["id"] = True

    url = (
        build_url(
            f"users/{self.data.username}/lists",
            fields=None,
            limit=limit,
            offset=offset,
        )
        + f"&fields=lists({construct_fields(dict(include_fields))})"  # ! Similar to story retrieval, requested fields need to be wrapped in `lists(<fields>)`.
    )
    data = cast(dict, await fetch_url(url))

    lists: set[List] = set()
    for list_ in data["lists"]:
        id_ = list_.pop("id")
        list_cls = List(
            id=id_, user=self
        )  # ! This code is an artefact of the singleton design model. If a list already exists, its data will not be updated otherwise.

        stories: set[Story] = set()
        for story in list_["stories"]:
            s_id_ = story.pop("id")
            story_cls = Story(id=s_id_)
            story_cls._update_data(**story)
            stories.add(story_cls)

        list_cls.stories.update(stories)
        lists.add(list_cls)

    self.lists.update(lists)
    self.data.num_lists = len(self.lists)

    return data

fetch_stories(include=False) async #

Fetch a User's authored stories.

Parameters:

Name Type Description Default
include bool | StoryModelFieldsType

Fields of authored stories to fetch. True fetches all fields. Defaults to False.

False

Returns:

Name Type Description
dict dict

The raw API Response.

Source code in src/wattpad/wattpad.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
async def fetch_stories(self, include: bool | StoryModelFieldsType = False) -> dict:
    """Fetch a User's authored stories.

    Args:
        include (bool | StoryModelFieldsType, optional): Fields of authored stories to fetch. True fetches all fields. Defaults to False.

    Returns:
        dict: The raw API Response.
    """
    if include is False:
        include_fields: StoryModelFieldsType = {}
    elif include is True:
        include_fields: StoryModelFieldsType = {
            key: True for key in get_fields(StoryModel)  # type: ignore
        }
    else:
        include_fields: StoryModelFieldsType = include

    include_fields["id"] = True

    if "tagRankings" in include_fields:
        if type(include_fields["tagRankings"]) is dict:
            include_fields["tagRankings"]["name"] = True

    if "parts" in include_fields:
        if type(include_fields["parts"]) is dict:
            include_fields["parts"]["id"] = True

    url = (
        build_url(f"users/{self.data.username}/stories", fields=None)
        + f"&fields=stories({construct_fields(dict(include_fields))})"  # ! The field format for story retrieval differs here. It's /stories?fields=stories(<fields>). Compared to the usual /path?fields=<fields>. For this reason, we need to manually edit the fields in.
    )
    data = cast(dict, await fetch_url(url))

    stories: list[Story] = []
    for story in data["stories"]:
        if "user" in story:
            story.pop("user")

        id_ = story.pop("id")

        story_cls = Story(
            user=self, id=id_
        )  # ! This code is an artefact of the singleton design model. If a user already exists, their data will not be updated otherwise.
        story_cls._update_data(**story)

        stories.append(story_cls)

    self.stories = stories
    self.data.num_stories_published = len(
        self.stories
    )  # ! The data['total'] can also be used, but it isn't always present. (Based on included_fields.)

    return data