TwitterAPI

The TwitterAPI class forms the main class for user interaction and serves as a simple interface through encapsulation since the user can use all the functionalities of the package via the four main functions. All functionalities (except the utility function from the utils module) can be used via this class.

The TwitterAPI class combines compatible data processing mechanisms with the respective data queries. This is achieved through composition whereby instances of the TwitterDataFetcher and TwitterDataProcessor are created within the class’s constructor.

Data processing mechanisms are outsourced to the functions of the TwitterDataProcessor instance whereas the data querying mechanisms are outsourced to the TwitterDataFetcher instance.

This design decision ensures hiding implementation details from users or developers during usage or implementation and makes the code more structured. Thus, whenever any developer wants to make adjustments/fixes to the code, he or she has not to be aware of the code from the TwitterDataProcessor and TwitterDataFetcher instances. Contributing developers are able to understand this class's implementation by understanding the seperation of classes' concerns.

This class serves the purpose of handling the input and output of user interactions through an interface. No further data processing or fetching is performed by this class.

This class is built on top of the tweepy.Client class. The TwitterAPI class inherits the tweepy.Client class. Thus, every function from the inherited class will also be available in the TwitterAPI class, resulting in an package that extends the official Twitter API. The default behavior of the tweepy.Client class is overwritten but retained in order to extend this class.

See the package architecture on the overview section of this chapter.

Initialization

Import the TwitterAPI class from the api module or use the shortcut.

from pysna.api import TwitterAPI # full path
from pysna import TwitterAPI # shortcut

api = TwitterAPI(
    bearer_token: Any | None = None,
    consumer_key: Any | None = None,
    consumer_secret: Any | None = None,
    access_token: Any | None = None,
    access_token_secret: Any | None = None,
    x_rapidapi_key: Any | None = None,
    x_rapidapi_host: Any | None = None,
    wait_on_rate_limit: bool = True
)

and invoke a function:

user_id = 123450897612
api.user_info(user_id, ["screen_name", "followers_count"])

Find the necessary secrets on the user guide instructions.

Methods

All functions have pre-defined input parameters (either for the attributes or compare arguments). They are stored inside the literal objects:

  • TwitterAPI.LITERALS_USER_INFO: available attributes for a Twitter account. For more information, see here.
  • TwitterAPI.LITERALS_COMPARE_USERS: available comparison attributes for Twitter accounts. For more information, see here.
  • TwitterAPI.LITERALS_TWEET_INFO: available attributes for a tweet. For more information, see here.
  • TwitterAPI.LITERALS_COMPARE_TWEETS: available comparison attributes for tweets. For more information, see here.

Each (comparison) attribute is covered in the corresponding main function by iterations of a for loop. Whenever one of the four main functions is called, the required data resources are fetched by making API calls via the TwitterDataFetcher class for each specified (comparison) attribute and are processed by the TwitterDataProcessor class if necessary. For instance, when comparing multiple users on their common followers, individual followers for each user are fetched first and then processed by calculating the intersection set of all individual followers.

If any new functionality is added to this class (e.g., get the length of a tweet by a length attribute), the new attribute has to be added to the corresponding literal object.

In addition, the comparison functions also have a features argument which is used to define a feature vector for comparison. The available similarity features for users and tweets are also specified in separate literal objects:

  • TwitterAPI.SIMILARITY_FEATURES_COMPARE_USERS: available similarity features for the TwitterAPI.compare_users function. For more information, see here.
  • TwitterAPI.SIMILARITY_FEATURES_COMPARE_TWEETS: available similarity features for the TwitterAPI.compare_tweets function. For more information, see here.

handle_output (private)

This function is designed to handle the output of the four main functions accordingly. To avoid accessing a returned dictionary with only one available key, this function returns either the single value from the one-key dictionary or the full dictionary itself if multiple keys are available.

Function:

TwitterAPI._handle_output(output: dict)

This function is private and, thus, not intended for external use. This function was designed to facilitate the use of the package without the need for accessing a returned one-key dictionary.


user_info

This function allows to request user information from a Twitter account.

Function:

TwitterAPI.user_info(user: str | int, attributes: List[LITERALS_USER_INFO] | str, return_timestamp: bool = False)

Args:

  • user (str | int): Twitter User either specified by corresponding ID or screen name.
  • attributes (List[str] | str): Attributes of the User object. These must be from this list.
  • return_timestamp (bool, optional): Add UTC Timestamp to results. Defaults to False.

This function takes in a Twitter user identifier (i.e., an ID or unique screen name). The attributes are passed in by a list object or by a single string.

For a single provided attribute, only the corresponding value is returned. For multiple attributes, a dictionary with the key-value pairs of the requested attributes is returned. If the requested attribute for the objet is not available, None will be returned.

Source Code
def user_info(self, user: str | int, attributes: List[LITERALS_USER_INFO] | str, return_timestamp: bool = False) -> Any:
    """Receive requested user information from Twitter User Object.

    For one attribute, only the corresponding value is returned. For multiple attributes, a dictionary with the key-value pairs of the requested attributes is returned.

    Args:
        user (str | int): Twitter User either specified by corresponding ID or screen name.
        attributes (List[str] | str): Attributes of the User object. These must be from: id, id_str, name, screen_name, followers, followees, location, description, url, entities, protected, followers_count, friends_count, listed_count, created_at, latest_activity, last_active, liked_tweets, composed_tweets, favourites_count, verified, statuses_count, status, contributors_enabled, profile_image_url_https, profile_banner_url, default_profile, default_profile_image, withheld_in_countries, bot_scores
        return_timestamp (bool, optional): Add UTC Timestamp to results. Defaults to False.

    Raises:
        KeyError: If invalid attribute was provided.
        ValueError: If Botometer secrets were not provided.

    Returns:
        dict: Requested user information.

    References: https://mathun3003.github.io/PySNA/user-guide/overview/TwitterAPI/#user_info
    """
    # catch Botometer API secrets before iteration over attributes.
    if "bot_scores" in attributes:
        if (self._x_rapidapi_key is None) or (self._x_rapidapi_host is None):
            raise ValueError("'X_RAPIDAPI_KEY' and 'X_RAPIDAPI_HOST' secrets for Botometer API need to be provided.")

    # initialize empty dict to store requested attributes
    user_info = dict()
    # if single string was provided
    if isinstance(attributes, str):
        # convert to list for iteration
        attributes = [attributes]
    # get user object
    user_obj = self.fetcher.get_user_object(user)
    # loop through the list of attributes and add them to the dictionary
    for attr in attributes:
        # if invalid attribute was provided
        if attr not in get_args(self.LITERALS_USER_INFO):
            raise ValueError("Invalid attribute for '{}'".format(attr))
        # if the desired attribute is in default user object returned by the v1 Search API
        elif attr in user_obj._json.keys():
            user_info[attr] = user_obj._json[attr]
        # get information about user's followers
        elif attr == "followers":
            user_info[attr] = self.data_processor.extract_followers(user_obj)
        # get information about user's followees
        elif attr == "followees":
            user_info[attr] = self.data_processor.extract_followees(user_obj)
        # get all liked tweets of user
        elif attr == "liked_tweets":
            # get page results first
            liked_tweets = self.fetcher.get_liked_tweets_ids(user)
            user_info[attr] = liked_tweets
        # get all composed tweets
        elif attr == "composed_tweets":
            # get page results first
            composed_tweets = self.fetcher.get_composed_tweets_ids(user)
            user_info[attr] = composed_tweets
        # get user's latest activity
        elif attr == "latest_activity":
            user_info[attr] = self.fetcher.get_latest_activity(user)
        # get user's latest activity date
        elif attr == "last_active":
            user_info[attr] = self.fetcher.get_latest_activity_date(user)
        # get user's botometer scores
        elif attr == "bot_scores":
            user_info[attr] = self.fetcher.get_botometer_scores(user)
        # if attribute was not found
        else:
            user_info[attr] = None
        # if timestamp should be returned
    if return_timestamp:
        user_info["utc_timestamp"] = strf_datetime(datetime.utcnow(), format="%Y-%m-%d %H:%M:%S.%f")

    return self._handle_output(user_info)

compare_users

This function allows a comparison of multiple Twitter accounts.

Function:

TwitterAPI.compare_users(users: List[str | int], compare: str | List[LITERALS_COMPARE_USERS], return_timestamp: bool = False, features: List[str] | None = None)

Args:

  • users (List[str | int]): User IDs or screen names
  • compare (str): Comparison attribute. Must be from this list.
  • return_timestamp (bool, optional): Add UTC Timestamp to results. Defaults to False.
  • features (List[str] | None, optional): Defined features of Twitter User Object on which similarity will be computed. Must be from the features list. Defaults to None.

This function takes in multiple Twitter user identifiers (i.e., IDs or unique screen names). The comparison attributes are passed in by a list object or by a single string.

For a single attribute, only the corresponding value is returned. For multiple attributes, a dictionary with the key-value pairs of the requested attributes is returned.

Source Code
def compare_users(self, users: List[str | int], compare: str | List[LITERALS_COMPARE_USERS], return_timestamp: bool = False, features: List[str] | None = None) -> Any:
        """Compare two or more users with the specified comparison attribute(s).

        For one attribute, only the corresponding value is returned. For multiple attributes, a dictionary with the key-value pairs of the requested attributes is returned.

        Args:
            users (List[str  |  int]): User IDs or screen names
            compare (str): Comparison attribute. Must be from: relationship, followers_count, followees_count, tweets_count, favourites_count, common_followers, distinct_followers, common_followees, distinct_followees, commonly_liked_tweets, distinctly_liked_tweets, similarity, created_at, protected, verified.
            return_timestamp (bool, optional): Add UTC Timestamp to results. Defaults to False.
            features (List[str] | None, optional): Defined features of Twitter User Object on which similarity will be computed. Must be from: followers_count, friends_count, listed_count, favourites_count, statuses_count. Defaults to None.

        Raises:
            ValueError: If invalid comparison attribute was provided.

        Returns:
            dict | list: Results of requested comparison attribute(s).

        Referencs: https://mathun3003.github.io/PySNA/user-guide/overview/TwitterAPI/#compare_users
        """
        # users list must contain at least two elements
        assert len(users) > 1, "'users' list must contain at least two elements, {} was/were provided".format(len(users))

        # catch if feature vector contains only numeric values, and contains at least two elements
        if features:
            assert len(features) > 1, "'features' list must have at least two elements. {} was/were given".format(len(features))
            for feat in features:
                if feat not in get_args(self.SIMILARITY_FEATURES_COMPARE_USERS):
                    raise ValueError(f"Only numeric features are supported. Must be from: {', '.join(get_args(self.SIMILARITY_FEATURES_COMPARE_USERS))}. You passed in {feat}")

        # if single comparison attribute was provided as string
        if isinstance(compare, str):
            # change to list object
            compare = [compare]
        # init empty dict to store results
        results = dict()
        # iterate over comparison attributes
        for attr in compare:
            # if invalid attribute was provided
            if attr not in get_args(self.LITERALS_COMPARE_USERS):
                raise ValueError("Invalid attribute for '{}'".format(attr))
            # match comparison attributes
            match attr:
                # compare relationships between two users
                case "relationship":
                    results[attr] = self.fetcher.get_relationship_pairs(users)
                # compare number of followers
                case "followers_count":
                    # get individual followers
                    followers = {user: self.fetcher.get_user_object(user).followers_count for user in users}
                    # add descriptive metrics
                    followers_with_metrics = self.data_processor.calc_descriptive_metrics(followers)
                    results[attr] = followers_with_metrics
                # compare number of friends
                case "followees_count":
                    # get individual followees
                    followees = {user: self.fetcher.get_user_object(user).friends_count for user in users}
                    # add descriptive metrics
                    followees = self.data_processor.calc_descriptive_metrics(followees)
                    results[attr] = followees
                # compare number of Tweets issued by each user
                case "tweets_count":
                    # get individual statuses counts
                    tweets = {user: self.fetcher.get_user_object(user).statuses_count for user in users}
                    # add descriptive metrics
                    tweets = self.data_processor.calc_descriptive_metrics(tweets)
                    results[attr] = tweets
                # compare number of likes issued by each user
                case "favourites_count":
                    # get individual likes
                    likes = {user: self.fetcher.get_user_object(user).favourites_count for user in users}
                    # add descriptive metrics
                    likes = self.data_processor.calc_descriptive_metrics(likes)
                    results[attr] = likes
                # compare protected attribute of users
                case "protected":
                    results[attr] = {user: self.fetcher.get_user_object(user).protected for user in users}
                # compare verified attribute for users
                case "verified":
                    results[attr] = {user: self.fetcher.get_user_object(user).verified for user in users}
                # get common followers
                case "common_followers":
                    # get individual followers first
                    individual_followers = [self.fetcher.get_user_follower_ids(user) for user in users]
                    # get common followers by calculating the intersection
                    common_followers = self.data_processor.intersection(individual_followers)
                    results[attr] = common_followers
                # get distinct followers
                case "distinct_followers":
                    # get individual followers first
                    individual_followers = {user: self.fetcher.get_user_follower_ids(user) for user in users}
                    # get distinct followers by calculating the difference of each set
                    distinct_followers = self.data_processor.difference(individual_followers)
                    results[attr] = distinct_followers
                # get common followees
                case "common_followees":
                    # get individual followees first
                    individual_followees = [self.fetcher.get_user_followee_ids(user) for user in users]
                    # get common followees by calculating the intersection
                    common_followees = self.data_processor.intersection(individual_followees)
                    results[attr] = common_followees
                # get distinct followees
                case "distinct_followees":
                    # get individual followees first
                    individual_followees = {user: self.fetcher.get_user_followee_ids(user) for user in users}
                    # get distinct followees by calculating the difference of each set
                    distinct_followees = self.data_processor.difference(individual_followees)
                    results[attr] = distinct_followees
                # get common liked tweets
                case "commonly_liked_tweets":
                    # get individual liked tweets first
                    individual_likes = [self.fetcher.get_liked_tweets_ids(user) for user in users]
                    # get common liked tweets by calculating the intersection
                    common_likes = self.data_processor.intersection(individual_likes)
                    results[attr] = common_likes
                # get distinct liked tweets
                case "distinctly_liked_tweets":
                    # get individual liked tweets first
                    individual_likes = {user: self.fetcher.get_liked_tweets_ids(user) for user in users}
                    # get distinct liked tweets by calculating the difference for each set
                    distinct_likes = self.data_processor.difference(individual_likes)
                    results[attr] = distinct_likes
                # compute similarity between two users basd on the defined features
                case "similarity":
                    # feature list object must be defined
                    if features is None:
                        raise ValueError("'features' list must be provided.")
                    # get serialized user objects first
                    user_objs = [self.fetcher.get_user_object(user)._json for user in users]
                    # calculate similarity based on defined feature vector
                    results[attr] = self.data_processor.calc_similarity(user_objs=user_objs, features=features)
                # compare creaation dates
                case "created_at":
                    # get individual creation dates first
                    creation_dates = {user: self.fetcher.get_user_object(user).created_at for user in users}
                    # add datetime metrics
                    creation_dates = self.data_processor.calc_datetime_metrics(creation_dates)
                    results[attr] = creation_dates
                # if comparison attribute was not found
                case _:
                    results[attr] = None
        # if timestamp should be returned
        if return_timestamp:
            results["utc_timestamp"] = strf_datetime(datetime.utcnow(), format="%Y-%m-%d %H:%M:%S.%f")

        return self._handle_output(results)

tweet_info

This function allows to request tweet information from a tweet object.

Function:

TwitterAPI.tweet_info(tweet_id: str | int, attributes: List[LITERALS_TWEET_INFO] | str, return_timestamp: bool = False)

Args:

  • tweet_id (str | int): Tweet ID
  • attributes (List[LITERALS_TWEET_INFO] | str): Attributes of the Tweet object. These must be from this list.
  • return_timestamp (bool, optional): Add UTC Timestamp to results. Defaults to False.

This function takes in a tweet ID as string or integer representation. The attributes are passed in by a list object or by a single string.

For a single provided attribute, only the corresponding value is returned. For multiple attributes, a dictionary with the key-value pairs of the requested attributes is returned. If the requested attribute for the objet is not available, None will be returned.

Source Code
def tweet_info(self, tweet_id: str | int, attributes: List[LITERALS_TWEET_INFO] | str, return_timestamp: bool = False) -> Any:
        """Receive requested Tweet information from Tweet Object.

        For one attribute, only the corresponding value is returned. For multiple attributes, a dictionary with the key-value pairs of the requested attributes is returned.

        Args:
            tweet_id (str | int): Tweet ID
            attributes (List[LITERALS_TWEET_INFO] | str): Attributes of the Tweet object. These must be from: id, id_str, full_text, display_text_range, truncated, created_at, entities, tweet_annotations, source, retweeters, in_reply_to_status_id, in_reply_to_status_id_str, in_reply_to_user_id, in_reply_to_user_id_str, in_reply_to_screen_name, user, contributors, coordinates, place, is_quote_status, public_metrics, quoting_users, liking_users, favorited, retweeted, retweeted_status, possibly_sensitive, lang, sentiment.
            return_timestamp (bool, optional): Add UTC Timestamp to results. Defaults to False.

        Raises:
            ValueError: If invalid attribute was provided.

        Returns:
            dict: Requested Tweet information.

        References: https://mathun3003.github.io/PySNA/user-guide/overview/TwitterAPI/#tweet_info
        """
        # get tweet object
        tweet_obj = self.fetcher.get_tweet_object(tweet_id)

        # initialize empty dict to store request information
        tweet_info = dict()

        # if single string was provided
        if isinstance(attributes, str):
            # convert to list for iteration
            attributes = [attributes]
        for attr in attributes:
            # if invalid attribute was provided
            if attr not in get_args(self.LITERALS_TWEET_INFO):
                raise ValueError("Invalid attribute for '{}'".format(attr))
            # get default attributes from tweepy Status model
            elif attr in tweet_obj._json.keys():
                tweet_info[attr] = tweet_obj._json[attr]
            # get all quoting users
            elif attr == "quoting_users":
                quoting_users = self.fetcher.get_quoting_users_ids(tweet_id)
                tweet_info[attr] = quoting_users
            # get all liking users
            elif attr == "liking_users":
                liking_users = self.fetcher.get_liking_users_ids(tweet_id)
                tweet_info[attr] = liking_users
            # get all retweeters
            elif attr == "retweeters":
                retweeters = self.fetcher.get_retweeters_ids(tweet_id)
                tweet_info[attr] = retweeters
            # get public metrics
            elif attr == "public_metrics":
                tweet_info[attr] = self.fetcher.get_public_metrics(tweet_id)
            # get context annotations
            elif attr == "tweet_annotations":
                tweet_info[attr] = self.fetcher.get_context_annotations_and_entities(tweet_id)
            # get tweet sentiment
            elif attr == "sentiment":
                tweet_info[attr] = self.data_processor.detect_tweet_sentiment(tweet_obj.full_text)
            # if attribute was not found
            else:
                tweet_info[attr] = None
        # if timestamp should be returned
        if return_timestamp:
            tweet_info["utc_timestamp"] = strf_datetime(datetime.utcnow(), format="%Y-%m-%d %H:%M:%S.%f")

        return self._handle_output(tweet_info)

compare_tweets

This function allows a comparison of multiple tweets.

Function:

TwitterAPI.compare_tweets(tweet_ids: List[str | int], compare: str | List[LITERALS_COMPARE_TWEETS], return_timestamp: bool = False, features: List[str] | None = None)

Args:

  • tweets (List[str | int]): List of Tweet IDs.
  • compare (str | List[LITERALS_COMPARE_TWEETS]): Comparison attribute. Needs to be from the this list.
  • return_timestamp (bool, optional): Add UTC Timestamp to results. Defaults to False.
  • features (List[str] | None, optional): Defined features of Twitter User Object on which similarity will be computed. Must be from the features list. Defaults to None.

This function takes in multiple tweet IDs as string or integer representation. The comparison attributes are passed in by a list object or by a single string.

For a single attribute, only the corresponding value is returned. For multiple attributes, a dictionary with the key-value pairs of the requested attributes is returned.

Source Code
def compare_tweets(self, tweet_ids: List[str | int], compare: str | List[LITERALS_COMPARE_TWEETS], return_timestamp: bool = False, features: List[str] | None = None) -> Any:
    """Compare two or more Tweets with the specified comparison attribute.

    For one attribute, only the corresponding value is returned. For multiple attributes, a dictionary with the key-value pairs of the requested attributes is returned.

    Args:
        tweets (List[str  |  int]): List of Tweet IDs.
        compare (str | List[LITERALS_COMPARE_TWEETS]): Comparison attribute. Needs to be from the following: view_count, like_count, retweet_count, quote_count, reply_count, common_quoting_users, distinct_quoting_users, common_liking_users, distinct_liking_users, common_retweeters, distinct_retweeters, similarity, created_at.
        return_timestamp (bool, optional): Add UTC Timestamp to results. Defaults to False.
        features (List[str] | None, optional): Defined features of Twitter User Object on which similarity will be computed. Must be from: retweet_count, reply_count, like_count, quote_count, impression_count. Defaults to None.

    Raises:
        AssertionError: If a list of one Tweet ID was provided.
        ValueError: If invalid comparison attribute was provided.

    Returns:
        dict: Requested results for comparison attribute.

    References: https://mathun3003.github.io/PySNA/user-guide/overview/TwitterAPI/#compare_tweets
    """
    # tweets list must contain at least two IDs
    assert len(tweet_ids) > 1, "'tweets' list object needs at least two entries, not {}".format(len(tweet_ids))

    # catch if feature vector contains only numeric values, and contains at least two elements
    if features:
        assert len(features) > 1, "'features' list must have at least two elements. {} was/were given".format(len(features))
        for feat in features:
            if feat not in get_args(self.SIMILARITY_FEATURES_COMPARE_TWEETS):
                raise ValueError(f"Only numeric features are supported. Must be from: {', '.join(get_args(self.SIMILARITY_FEATURES_COMPARE_TWEETS))}. You passed in {feat}.")

    # if single comparison attribute was provided as string
    if isinstance(compare, str):
        # change to list object
        compare = [compare]
    # init empty dict to store results
    results = dict()
    # iterate over every given comparison atttribute
    for attr in compare:
        # if invalid attribute was provided
        if attr not in get_args(self.LITERALS_COMPARE_TWEETS):
            raise ValueError("Invalid attribute for '{}'".format(attr))
        # match comparison attribute
        match attr:
            # compare numer of views / impressions
            case "view_count":
                # get individual view_counts
                view_counts = {tweet_id: self.fetcher.get_public_metrics(tweet_id)["impression_count"] for tweet_id in tweet_ids}
                # add descriptive metrics
                view_counts = self.data_processor.calc_descriptive_metrics(view_counts)
                results[attr] = view_counts
            # compare number of likes
            case "like_count":
                # get individual like_counts
                like_counts = {tweet_id: self.fetcher.get_public_metrics(tweet_id)["like_count"] for tweet_id in tweet_ids}
                # add descriptive metrics
                like_counts = self.data_processor.calc_descriptive_metrics(like_counts)
                results[attr] = like_counts
            # compare number or retweets
            case "retweet_count":
                # get individual number of retweets
                retweet_counts = {tweet_id: self.fetcher.get_public_metrics(tweet_id)["retweet_count"] for tweet_id in tweet_ids}
                # add descriptive metrics
                retweet_counts = self.data_processor.calc_descriptive_metrics(retweet_counts)
                results[attr] = retweet_counts
            # compare number of quotes
            case "quote_count":
                # get individual number of quotes
                quote_counts = {tweet_id: self.fetcher.get_public_metrics(tweet_id)["quote_count"] for tweet_id in tweet_ids}
                # add descriptive metrics
                quote_counts = self.data_processor.calc_descriptive_metrics(quote_counts)
                results[attr] = quote_counts
            # compare number of commonts
            case "reply_count":
                # get individual number of replies first
                reply_counts = {tweet_id: self.fetcher.get_public_metrics(tweet_id)["reply_count"] for tweet_id in tweet_ids}
                # add descriptive metrics
                reply_counts = self.data_processor.calc_descriptive_metrics(reply_counts)
                results[attr] = reply_counts
            # get all quoting users all Tweets have in common
            case "common_quoting_users":
                # get individual quoting users first
                quoting_users = [self.fetcher.get_quoting_users_ids(tweet_id) for tweet_id in tweet_ids]
                # get common quoting users by calculating the intersection
                common_quoting_users = self.data_processor.intersection(quoting_users)
                # return quoting users
                results[attr] = common_quoting_users
            # get distinct quoting users for each tweet
            case "distinct_quoting_users":
                # get individual quoting users first
                quoting_users = {tweet_id: self.fetcher.get_quoting_users_ids(tweet_id) for tweet_id in tweet_ids}
                # get distinct quoting users for each tweet by calculating the difference for each set
                distinct_quoting_users = self.data_processor.difference(quoting_users)
                results[attr] = distinct_quoting_users
            # get all liking users that all tweets have in common
            case "common_liking_users":
                # get individual liking users first
                liking_users = [self.fetcher.get_liking_users_ids(tweet_id) for tweet_id in tweet_ids]
                # get common liking users by calculating the intersection
                common_liking_users = self.data_processor.intersection(liking_users)
                # return common liking users
                results[attr] = common_liking_users
            # get distinct liking users of all tweets
            case "distinct_liking_users":
                # get individual liking users first
                liking_users = {tweet_id: self.fetcher.get_liking_users_ids(tweet_id) for tweet_id in tweet_ids}
                # get distinct liking users for each tweet by calculating the difference for each set
                distinct_liking_users = self.data_processor.difference(liking_users)
                results[attr] = distinct_liking_users
            # get all retweeters all tweets have in common
            case "common_retweeters":
                # get individual retweeters first
                retweeters = [self.fetcher.get_retweeters_ids(tweet_id) for tweet_id in tweet_ids]
                # get common retweeters by calculating the intersection
                common_retweeters = self.data_processor.intersection(retweeters)
                # return common retweeters
                results[attr] = common_retweeters
            # get distinct retweeters of all tweets
            case "distinct_retweeters":
                # get individual retweeters first
                retweeters = {tweet_id: self.fetcher.get_retweeters_ids(tweet_id) for tweet_id in tweet_ids}
                # get distinct retweeters by calculating the difference for each set
                distinct_retweeters = self.data_processor.difference(retweeters)
                results[attr] = distinct_retweeters
            # compute similarity between two tweets basd on the defined features
            case "similarity":
                # feature list object must be defined
                if features is None:
                    raise ValueError("'features' list must be provided.")
                # get public metrics for Tweet objects first
                public_metrics = {tweet_id: self.fetcher.get_public_metrics(tweet_id) for tweet_id in tweet_ids}
                # calculate similarity based on defined feature vector
                results[attr] = self.data_processor.calc_similarity(tweet_metrics=public_metrics, features=features)
            # compare creation dates of tweets
            case "created_at":
                # get individual creation dates first
                creation_dates = {tweet_id: self.fetcher.get_tweet_object(tweet_id).created_at for tweet_id in tweet_ids}
                # add datetime metrics
                creation_dates = self.data_processor.calc_datetime_metrics(creation_dates)
                results[attr] = creation_dates
            # if attribute was not found
            case _:
                results[attr] = None
    # if UTC timestamp should be returned
    if return_timestamp:
        results["utc_timestamp"] = strf_datetime(datetime.utcnow(), format="%Y-%m-%d %H:%M:%S.%f")

    return self._handle_output(results)