diff --git a/Knew Karma/KnewKarma/Utilities/CoreUtils.vb b/Knew Karma/KnewKarma/Assets/CoreUtils.vb similarity index 87% rename from Knew Karma/KnewKarma/Utilities/CoreUtils.vb rename to Knew Karma/KnewKarma/Assets/CoreUtils.vb index 8fe7ab1..ede95df 100644 --- a/Knew Karma/KnewKarma/Utilities/CoreUtils.vb +++ b/Knew Karma/KnewKarma/Assets/CoreUtils.vb @@ -62,33 +62,6 @@ Public Class CoreUtils End If End Function - ''' - ''' Asynchronously checks for available updates and optionally displays a message to the user. - ''' - ''' Indicates whether the update check is triggered automatically. - ''' A task representing the asynchronous operation. - Public Shared Async Function AsyncCheckUpdates() As Task - AboutWindow.Version.Text = "Checking for Updates..." - ' Creating a new instance of the ApiHandler class to interact with the API. - Dim Api As New ApiHandler() - - ' Making an asynchronous request to check for updates. - Dim data As JObject = Await Api.AsyncGetUpdates() - - ' Checking if data is not null before proceeding with extracting information from it. - If data IsNot Nothing Then - ' Extracting the tag name, body, and HTML URL from the data. - Dim tagName As String = data("tag_name")?.ToString - - ' Checking if the current version is the latest version. - If tagName = My.Application.Info.Version.ToString Then - AboutWindow.Version.Text = $"Up-to-date: {My.Application.Info.Version}" - Else - AboutWindow.Version.Text = $"Updates found: {tagName}" - AboutWindow.ButtonGetUpdates.Enabled = True - End If - End If - End Function ''' ''' Checks whether the given JSON data (either JObject or JArray) is null or empty. @@ -107,11 +80,11 @@ Public Class CoreUtils End Function ''' - ''' Converts a Unix timestamp with possible decimal points to a formatted datetime string. + ''' Converts a Unix timestamp with possible decimal points to a formatted datetime.utc string. ''' ''' The Unix timestamp to be converted. ''' A formatted datetime string in the format "dd MMMM yyyy, hh:mm:ss.fff tt". - Public Shared Function ConvertTimestampToDatetime(ByVal timestamp As Double) As String + Public Shared Function UnixTimestampToUtc(ByVal timestamp As Double) As String Dim utcFromTimestamp As Date = New DateTime( 1970, 1, diff --git a/Knew Karma/KnewKarma/Utilities/Settings.vb b/Knew Karma/KnewKarma/Assets/Settings.vb similarity index 100% rename from Knew Karma/KnewKarma/Utilities/Settings.vb rename to Knew Karma/KnewKarma/Assets/Settings.vb diff --git a/Knew Karma/KnewKarma/Handlers/ApiHandler.vb b/Knew Karma/KnewKarma/Handlers/ApiHandler.vb index 1996b6a..ca37f3b 100644 --- a/Knew Karma/KnewKarma/Handlers/ApiHandler.vb +++ b/Knew Karma/KnewKarma/Handlers/ApiHandler.vb @@ -62,8 +62,52 @@ Public Class ApiHandler ''' ''' Asynchronously fetches the program's update information from GitHub. ''' - Public Async Function AsyncGetUpdates() As Task(Of JObject) - Return Await AsyncGetData(endpoint:="https://api.github.com/repos/bellingcat/knewkarma/releases/latest") + Public Async Function AsyncGetUpdates() As Task + AboutWindow.Version.Text = "Checking for updates..." + ' Making an asynchronous request to check for updates. + Dim data As JObject = Await AsyncGetData(endpoint:="https://api.github.com/repos/rly0nheart/knewkarma/releases/latest") + + ' Checking if data is not null before proceeding with extracting information from it. + If data IsNot Nothing Then + ' Extracting the tag name, body, and HTML URL from the data. + Dim tagName As String = data("tag_name")?.ToString + + ' Checking if the current version is the latest version. + If tagName = My.Application.Info.Version.ToString Then + AboutWindow.Version.Text = $"Up-to-date: {My.Application.Info.Version}" + Else + AboutWindow.Version.Text = $"Updates found: {tagName}" + AboutWindow.ButtonGetUpdates.Enabled = True + End If + End If + End Function + + + ''' + ''' Asynchronously retrieves profile data from a specified source. + ''' + ''' The type of profile to retrieve. + ''' The source from where the profile should be fetched (e.g., specific user or subreddit). + ''' A Task(Of JObject) representing the asynchronous operation, which upon completion returns a Jobject of profile data. + Public Async Function AsyncGetProfile( + profileType As String, profileSource As String) As Task(Of JObject) + Dim profileTypeMap As New List(Of Tuple(Of String, String)) From { + Tuple.Create("user_profile", $"{BASE_ENDPOINT}/user/{profileSource}/about.json"), + Tuple.Create("subreddit_profile", $"{BASE_ENDPOINT}/r/{profileSource}/about.json") + } + + Dim profileEndpoint As String = Nothing + + For Each Type In profileTypeMap + If Type.Item1 = profileType Then + profileEndpoint = Type.Item2 + Exit For + End If + Next + + Dim profile As JObject = Await AsyncGetData(endpoint:=profileEndpoint) + + Return If(profile IsNot Nothing AndAlso profile?("data") IsNot Nothing, profile?("data"), New JObject()) End Function @@ -95,23 +139,13 @@ Public Class ApiHandler End If Dim postsEndpoint As String = postsTypeMap(postsType) - Return Await PaginatedPosts(postsEndpoint, postsLimit) - End Function - - ''' - ''' Retrieves posts in a paginated manner until the specified limit is reached. - ''' - ''' The API endpoint for retrieving posts. - ''' The limit on the number of posts to retrieve. - ''' A Task(Of JArray) representing the asynchronous operation, which upon completion returns a JArray of posts. - Private Async Function PaginatedPosts(endpoint As String, limit As Integer) As Task(Of JArray) Dim allPosts As New JArray() - Dim lastPostId As String = "" - Dim useAfter As Boolean = limit > 100 + Dim lastPostId As String = String.Empty + Dim paginatePosts As Boolean = postsLimit > 100 - While allPosts.Count < limit - Dim endpointWithAfter As String = If(useAfter And Not String.IsNullOrEmpty(lastPostId), $"{endpoint}&after={lastPostId}", endpoint) - Dim postsData As JObject = Await AsyncGetData(endpoint:=endpointWithAfter) + While allPosts.Count < postsLimit + Dim paginationEndpoint As String = If(paginatePosts And Not String.IsNullOrEmpty(lastPostId), $"{postsEndpoint}&after={lastPostId}", postsEndpoint) + Dim postsData As JObject = Await AsyncGetData(endpoint:=paginationEndpoint) Dim postsChildren As JArray = postsData("data")("children") If postsChildren.Count = 0 Then @@ -125,32 +159,5 @@ Public Class ApiHandler Return allPosts End Function - - - ''' - ''' Asynchronously retrieves profile data from a specified source. - ''' - ''' The type of profile to retrieve. - ''' The source from where the profile should be fetched (e.g., specific user or subreddit). - ''' A Task(Of JObject) representing the asynchronous operation, which upon completion returns a Jobject of profile data. - Public Async Function AsyncGetProfile( - profileType As String, profileSource As String) As Task(Of JObject) - Dim profileTypeMap As New List(Of Tuple(Of String, String)) From { - Tuple.Create("user_profile", $"{BASE_ENDPOINT}/user/{profileSource}/about.json"), - Tuple.Create("subreddit_profile", $"{BASE_ENDPOINT}/r/{profileSource}/about.json") - } - - Dim profileEndpoint As String = Nothing - - For Each Type In profileTypeMap - If Type.Item1 = profileType Then - profileEndpoint = Type.Item2 - Exit For - End If - Next - - Dim profile As JObject = Await AsyncGetData(endpoint:=profileEndpoint) - - Return If(profile IsNot Nothing AndAlso profile?("data") IsNot Nothing, profile?("data"), New JObject()) - End Function End Class + diff --git a/Knew Karma/KnewKarma/Handlers/DataGridViewHandler.vb b/Knew Karma/KnewKarma/Handlers/DataGridViewHandler.vb index 9cd5255..e8049b0 100644 --- a/Knew Karma/KnewKarma/Handlers/DataGridViewHandler.vb +++ b/Knew Karma/KnewKarma/Handlers/DataGridViewHandler.vb @@ -51,7 +51,7 @@ Public Class DataGridViewer ' Special handling for the "created" timestamp If RowKey = "created" Then Dim timestamp As Double = CDbl(data("data")(RowKey)) - value = CoreUtils.ConvertTimestampToDatetime(timestamp) + value = CoreUtils.UnixTimestampToUtc(timestamp:=timestamp) Else value = If(data("data")(RowKey)?.ToString(), "N/A") End If diff --git a/Knew Karma/KnewKarma/KnewKarma.vbproj b/Knew Karma/KnewKarma/KnewKarma.vbproj index ace5c61..0e3fef0 100644 --- a/Knew Karma/KnewKarma/KnewKarma.vbproj +++ b/Knew Karma/KnewKarma/KnewKarma.vbproj @@ -12,11 +12,11 @@ https://github.com/bellingcat/knewkarma/wiki README.md https://github.com/bellingcat/knewkarma - 3.2.0.0 - 3.2.0.0 + 3.3.0.0 + 3.3.0.0 LICENSE True - 3.2.0 + 3.3.0 reddit;scraper;reddit-scraper;osint;reddit-data 6.0-recommended diff --git a/Knew Karma/KnewKarma/KnewKarma.vbproj.user b/Knew Karma/KnewKarma/KnewKarma.vbproj.user index ee29f88..471fe9d 100644 --- a/Knew Karma/KnewKarma/KnewKarma.vbproj.user +++ b/Knew Karma/KnewKarma/KnewKarma.vbproj.user @@ -13,6 +13,9 @@ Form + + Form + Form diff --git a/Knew Karma/KnewKarma/README.md b/Knew Karma/KnewKarma/README.md index e6cf90a..79959a3 100644 --- a/Knew Karma/KnewKarma/README.md +++ b/Knew Karma/KnewKarma/README.md @@ -1,27 +1,139 @@ -![carbon](https://github.com/bellingcat/knewkarma/assets/74001397/a7165335-89d4-4632-b3af-2f3bb03082bb) +![knewkarma](https://github.com/bellingcat/knewkarma/assets/74001397/45262d9d-6633-418d-9ace-7c3c88b5ca36) + A **Reddit** Data Analysis Toolkit. [![.Net](https://img.shields.io/badge/Visual%20Basic%20.NET-5C2D91?style=flat&logo=.net&logoColor=white)](https://github.com/search?q=repo%3Abellingcat%2Fknewkarma++language%3A%22Visual+Basic+.NET%22&type=code) [![Python](https://img.shields.io/badge/Python-3670A0?style=flat&logo=python&logoColor=ffdd54)](https://github.com/search?q=repo%3Abellingcat%2Fknewkarma++language%3APython&type=code) [![Docker](https://img.shields.io/badge/Dockefile-%230db7ed.svg?style=flat&logo=docker&logoColor=white)](https://github.com/search?q=repo%3Abellingcat%2Fknewkarma++language%3ADockerfile&type=code) [![PyPI - Version](https://img.shields.io/pypi/v/knewkarma?style=flat&logo=pypi&logoColor=ffdd54&label=PyPI&labelColor=3670A0&color=3670A0)](https://pypi.org/project/knewkarma) [![BuyMeACoffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=flat&logo=buy-me-a-coffee&logoColor=black)](https://buymeacoffee.com/_rly0nheart) +*** # Feature Overview -* **Knew Karma can get the following Reddit data from individual targets**: +## Knew Karma CLI/GUI + +- [x] **Knew Karma can get the following Reddit data from individual targets**: * **User**: *Profile*, *Posts*, *Comments* * **Subreddit**: *Profile*, *Posts* - * **Post**: *Data*, *Comments* (available only in the CLI) -* **It can also get posts from various sources, such as**: +- [x] **It can also get posts from various sources, such as**: * **Searching**: Allows getting posts that match the user-provided query from all over Reddit * **Reddit Front-Page**: Allows getting posts from the Reddit Front-Page * **Listing**: Allows getting posts from a user-specified Reddit Listing -* **Bonus Features** - * **CLI/GUI** +- [x] **Bonus Features** + * **Fully Async (both in the CLI and GUI)** * **Dark Mode** (*GUI Automatic/Manual*) * **Write data to files** (*JSON/CSV*) +## Knew Karma Python Library + +
+ Code Examples + +### Get User Data + +```python +import asyncio +import aiohttp +from knewkarma import RedditUser + + +# Define an asynchronous function to fetch User +async def async_user(username: str, data_timeframe: str, data_limit: int, data_sort: str): + # Initialize a RedditUser object with the specified username, data timeframe, limit, and sorting criteria + user = RedditUser(username=username, data_timeframe=data_timeframe, data_limit=data_limit, data_sort=data_sort) + + # Establish an asynchronous HTTP session + async with aiohttp.ClientSession() as session: + # Fetch user's profile + profile = await user.profile(session=session) + + # Fetch user's posts + posts = await user.posts(session=session) + + # Fetch user's comments + comments = await user.comments(session=session) + + print(profile) + print(posts) + print(comments) + + +# Run the asynchronous function with a specified username, data limit, and sorting parameter +# timeframes: ["all", "hour", "day", "month", "year"] +# sorting: ["all", "controversial", "new", "top", "best", "hot", "rising"] +asyncio.run(async_user(username="automoderator", data_timeframe="year", data_limit=100, data_sort="all")) +``` + +### Get Subreddit Data + +````python +import asyncio +import aiohttp +from knewkarma import RedditSub + + +async def async_subreddit(subreddit_name: str, data_timeframe: str, data_limit: int, data_sort: str): + # Initialize a RedditSub object with the specified subreddit, data timeframe, limit, and sorting criteria + subreddit = RedditSub( + subreddit=subreddit_name, data_timeframe=data_timeframe, data_limit=data_limit, data_sort=data_sort + ) + + # Create an asynchronous HTTP session + async with aiohttp.ClientSession() as session: + # Fetch subreddit's profile + profile = await subreddit.profile(session=session) + + # Fetch subreddit's posts + posts = await subreddit.posts(session=session) + + print(profile) + print(posts) + + +# Run the asynchronous function with specified subreddit name, data limit, and sorting criteria +# timeframes: ["all", "hour", "day", "month", "year"] +# sorting: ["all", "controversial", "new", "top", "best", "hot", "rising"] +asyncio.run( + async_subreddit(subreddit_name="MachineLearning", data_timeframe="year", data_limit=100, data_sort="top") +) +```` + +### Get Posts + +```python +import asyncio +import aiohttp +from knewkarma import RedditPosts + + +async def async_posts(timeframe: str, limit: int, sort: str): + # Initialize RedditPosts with the specified timeframe, limit and sorting criteria + posts = RedditPosts(timeframe=timeframe, limit=limit, sort=sort) + + # Create an asynchronous HTTP session + async with aiohttp.ClientSession() as session: + # Fetch front page posts + front_page_posts = await posts.front_page(session=session) + # Fetch posts from a specified listing ('best') + listing_posts = await posts.listing(listings_name="best", session=session) + # Fetch posts that match the specified search query 'covid-19' + search_results = await posts.search(query="covid-19", session=session) + + print(front_page_posts) + print(listing_posts) + print(search_results) + + +# Run the asynchronous function with a specified limit and sorting parameter +# timeframes: ["all", "hour", "day", "month", "year"] +# sorting: ["all", "controversial", "new", "top", "best", "hot", "rising"] +asyncio.run(async_posts(timeframe="year", limit=100, sort="all")) +``` + +
+ # Documentation *[Refer to the Wiki](https://github.com/bellingcat/knewkarma/wiki) for Installation, Usage and Uninstallation instructions.* *** [![me](https://github.com/bellingcat/knewkarma/assets/74001397/efd19c7e-9840-4969-b33c-04087e73e4da)](https://about.me/rly0nheart) + diff --git a/Knew Karma/KnewKarma/Windows/AboutWindow.vb b/Knew Karma/KnewKarma/Windows/AboutWindow.vb index b9cc9d0..906c745 100644 --- a/Knew Karma/KnewKarma/Windows/AboutWindow.vb +++ b/Knew Karma/KnewKarma/Windows/AboutWindow.vb @@ -22,7 +22,8 @@ 'Version.Text = $"Version {My.Application.Info.Version}" Copyright.Text = My.Application.Info.Copyright - Await CoreUtils.AsyncCheckUpdates() + Dim ApiHandler As New ApiHandler() + Await ApiHandler.AsyncGetUpdates() End Sub ''' diff --git a/Knew Karma/KnewKarma/Windows/MainWindow.Designer.vb b/Knew Karma/KnewKarma/Windows/MainWindow.Designer.vb index dc7a345..aaa5463 100644 --- a/Knew Karma/KnewKarma/Windows/MainWindow.Designer.vb +++ b/Knew Karma/KnewKarma/Windows/MainWindow.Designer.vb @@ -23,7 +23,7 @@ Partial Class MainWindow Private Sub InitializeComponent() components = New ComponentModel.Container() - Dim resources As ComponentModel.ComponentResourceManager = New ComponentModel.ComponentResourceManager(GetType(MainWindow)) + Dim resources As System.ComponentModel.ComponentResourceManager = New System.ComponentModel.ComponentResourceManager(GetType(MainWindow)) Dim TreeNode1 As TreeNode = New TreeNode("Subreddit") Dim TreeNode2 As TreeNode = New TreeNode("User") Dim TreeNode3 As TreeNode = New TreeNode("Front Page") @@ -495,7 +495,7 @@ Partial Class MainWindow NumericUpDownSearchResultLimit.Font = New Font("Segoe UI Variable Display Semib", 8.25F, FontStyle.Bold, GraphicsUnit.Point) NumericUpDownSearchResultLimit.Location = New Point(130, 41) NumericUpDownSearchResultLimit.Maximum = New Decimal(New Integer() {10000, 0, 0, 0}) - NumericUpDownSearchResultLimit.Minimum = New Decimal(New Integer() {5, 0, 0, 0}) + NumericUpDownSearchResultLimit.Minimum = New Decimal(New Integer() {100, 0, 0, 0}) NumericUpDownSearchResultLimit.Name = "NumericUpDownSearchResultLimit" NumericUpDownSearchResultLimit.ReadOnly = True NumericUpDownSearchResultLimit.Size = New Size(79, 22) @@ -541,7 +541,7 @@ Partial Class MainWindow NumericUpDownFrontPageDataLimit.Font = New Font("Segoe UI Variable Text Semibold", 8.25F, FontStyle.Bold, GraphicsUnit.Point) NumericUpDownFrontPageDataLimit.Location = New Point(130, 41) NumericUpDownFrontPageDataLimit.Maximum = New Decimal(New Integer() {10000, 0, 0, 0}) - NumericUpDownFrontPageDataLimit.Minimum = New Decimal(New Integer() {5, 0, 0, 0}) + NumericUpDownFrontPageDataLimit.Minimum = New Decimal(New Integer() {100, 0, 0, 0}) NumericUpDownFrontPageDataLimit.Name = "NumericUpDownFrontPageDataLimit" NumericUpDownFrontPageDataLimit.ReadOnly = True NumericUpDownFrontPageDataLimit.Size = New Size(79, 22) @@ -600,7 +600,7 @@ Partial Class MainWindow NumericUpDownSubredditPostsLimit.Font = New Font("Segoe UI Variable Display Semib", 8.25F, FontStyle.Bold, GraphicsUnit.Point) NumericUpDownSubredditPostsLimit.Location = New Point(130, 41) NumericUpDownSubredditPostsLimit.Maximum = New Decimal(New Integer() {10000, 0, 0, 0}) - NumericUpDownSubredditPostsLimit.Minimum = New Decimal(New Integer() {5, 0, 0, 0}) + NumericUpDownSubredditPostsLimit.Minimum = New Decimal(New Integer() {100, 0, 0, 0}) NumericUpDownSubredditPostsLimit.Name = "NumericUpDownSubredditPostsLimit" NumericUpDownSubredditPostsLimit.ReadOnly = True NumericUpDownSubredditPostsLimit.Size = New Size(79, 22) @@ -675,7 +675,7 @@ Partial Class MainWindow NumericUpDownUserDataLimit.Font = New Font("Segoe UI Variable Display Semib", 8.25F, FontStyle.Bold, GraphicsUnit.Point) NumericUpDownUserDataLimit.Location = New Point(130, 41) NumericUpDownUserDataLimit.Maximum = New Decimal(New Integer() {10000, 0, 0, 0}) - NumericUpDownUserDataLimit.Minimum = New Decimal(New Integer() {5, 0, 0, 0}) + NumericUpDownUserDataLimit.Minimum = New Decimal(New Integer() {100, 0, 0, 0}) NumericUpDownUserDataLimit.Name = "NumericUpDownUserDataLimit" NumericUpDownUserDataLimit.ReadOnly = True NumericUpDownUserDataLimit.Size = New Size(79, 22) @@ -927,7 +927,7 @@ Partial Class MainWindow NumericUpDownPostListingsLimit.Font = New Font("Segoe UI Variable Display Semib", 8.25F, FontStyle.Bold, GraphicsUnit.Point) NumericUpDownPostListingsLimit.Location = New Point(130, 41) NumericUpDownPostListingsLimit.Maximum = New Decimal(New Integer() {10000, 0, 0, 0}) - NumericUpDownPostListingsLimit.Minimum = New Decimal(New Integer() {5, 0, 0, 0}) + NumericUpDownPostListingsLimit.Minimum = New Decimal(New Integer() {100, 0, 0, 0}) NumericUpDownPostListingsLimit.Name = "NumericUpDownPostListingsLimit" NumericUpDownPostListingsLimit.ReadOnly = True NumericUpDownPostListingsLimit.Size = New Size(79, 22) diff --git a/Knew Karma/KnewKarmaSetup/KnewKarmaSetup.vdproj b/Knew Karma/KnewKarmaSetup/KnewKarmaSetup.vdproj index c1eeb53..4d62f4c 100644 --- a/Knew Karma/KnewKarmaSetup/KnewKarmaSetup.vdproj +++ b/Knew Karma/KnewKarmaSetup/KnewKarmaSetup.vdproj @@ -229,15 +229,15 @@ { "Name" = "8:Microsoft Visual Studio" "ProductName" = "8:Knew Karma" - "ProductCode" = "8:{73004786-0578-4532-B92C-A9D8A8B5E8B2}" - "PackageCode" = "8:{9ECB32FA-4D0C-4D29-9723-17958851D59F}" + "ProductCode" = "8:{EC317D6E-D0D6-4C8B-9120-59D6F8AE1626}" + "PackageCode" = "8:{5A52E59B-8C23-4400-8D0F-851C28E3DADA}" "UpgradeCode" = "8:{9B03AD0F-0C14-4075-AB75-01CD38A594B4}" "AspNetVersion" = "8:2.0.50727.0" "RestartWWWService" = "11:FALSE" "RemovePreviousVersions" = "11:TRUE" "DetectNewerInstalledVersion" = "11:TRUE" "InstallAllUsers" = "11:FALSE" - "ProductVersion" = "8:3.2.0" + "ProductVersion" = "8:3.3.0" "Manufacturer" = "8:Richard Mwewa" "ARPHELPTELEPHONE" = "8:" "ARPHELPLINK" = "8:https://github.com/bellingcat/knewkarma/wiki" @@ -777,7 +777,7 @@ { "{5259A561-127C-4D43-A0A1-72F10C7B3BF8}:_6E15D9F422094BD9809550AF1BA1C161" { - "SourcePath" = "8:..\\KnewKarma\\obj\\Release\\net6.0-windows\\apphost.exe" + "SourcePath" = "8:..\\KnewKarma\\obj\\Debug\\net6.0-windows\\apphost.exe" "TargetName" = "8:" "Tag" = "8:" "Folder" = "8:_C0F76EDD899B4FFF80C2AC1B5526BC22" diff --git a/knewkarma/_cli.py b/knewkarma/_cli.py index 48457ac..bca61a3 100644 --- a/knewkarma/_cli.py +++ b/knewkarma/_cli.py @@ -113,6 +113,8 @@ async def setup_cli(arguments: argparse.Namespace): save_csv=arguments.csv, ) + break + # -------------------------------------------------------------------- # if not is_executed: @@ -124,7 +126,7 @@ async def setup_cli(arguments: argparse.Namespace): # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # -def execute_cli(): +def execute(): """Main entrypoint for the Knew Karma command-line interface.""" # -------------------------------------------------------------------- # diff --git a/knewkarma/_coreutils.py b/knewkarma/_coreutils.py index 08bae58..e448eee 100644 --- a/knewkarma/_coreutils.py +++ b/knewkarma/_coreutils.py @@ -4,6 +4,7 @@ import json import logging import os +from datetime import datetime from typing import Union, List from ._parser import create_parser @@ -13,15 +14,13 @@ # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # -def timestamp_to_utc(timestamp: int) -> str: +def unix_timestamp_to_utc(timestamp: int) -> str: """ Converts a UNIX timestamp to a formatted datetime.utc string. :param timestamp: The UNIX timestamp to be converted. :return: A formatted datetime.utc string in the format "dd MMMM yyyy, hh:mm:ssAM/PM" """ - from datetime import datetime - utc_from_timestamp: datetime = datetime.utcfromtimestamp(timestamp) datetime_string: str = utc_from_timestamp.strftime("%d %B %Y, %I:%M:%S%p") @@ -31,6 +30,30 @@ def timestamp_to_utc(timestamp: int) -> str: # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # +def filename_timestamp() -> str: + """ + Generates a timestamp string suitable for file naming, based on the current date and time. + The format of the timestamp is adapted based on the operating system. + + :return: The formatted timestamp as a string. The format is "%d-%B-%Y-%I-%M-%S%p" for Windows + and "%d-%B-%Y-%I:%M:%S%p" for non-Windows systems. + + Example + ------- + - Windows: "20-July-1969-08-17-45PM" + - Non-Windows: "20-July-1969-08:17:45PM" (format may vary based on the current date and time) + """ + now = datetime.now() + return ( + now.strftime("%d-%B-%Y-%I-%M-%S%p") + if os.name == "nt" + else now.strftime("%d-%B-%Y-%I:%M:%S%p") + ) + + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # + + def pathfinder(directories: list[str]): """ Creates directories in knewkarma-data directory of the user's home folder. @@ -47,8 +70,8 @@ def pathfinder(directories: list[str]): def save_data( data: Union[User, Subreddit, List[Union[Post, Comment]]], save_to_dir: str, - save_json: bool = False, - save_csv: bool = False, + save_json: Union[bool, str] = False, + save_csv: Union[bool, str] = False, ): """ Save the given (Reddit) data to a JSON/CSV file based on the save_csv and save_json parameters. @@ -74,7 +97,9 @@ def save_data( # -------------------------------------------------------------------- # if save_json: - json_path = os.path.join(save_to_dir, "json", f"{save_json}.json") + json_path = os.path.join( + save_to_dir, "json", f"{save_json.upper()}-{filename_timestamp()}.json" + ) with open(json_path, "w", encoding="utf-8") as json_file: json.dump(function_data, json_file, indent=4) log.info( @@ -84,7 +109,9 @@ def save_data( # -------------------------------------------------------------------- # if save_csv: - csv_path = os.path.join(save_to_dir, "csv", f"{save_csv}.csv") + csv_path = os.path.join( + save_to_dir, "csv", f"{save_csv.upper()}-{filename_timestamp()}.csv" + ) with open(csv_path, "w", newline="", encoding="utf-8") as csv_file: writer = csv.writer(csv_file) if isinstance(function_data, dict): diff --git a/knewkarma/_project.py b/knewkarma/_project.py index d0be520..775e814 100644 --- a/knewkarma/_project.py +++ b/knewkarma/_project.py @@ -6,7 +6,7 @@ author: str = "Richard Mwewa" about_author: str = "https://about.me/rly0nheart" -version: str = "3.2.0.0" +version: str = "3.3.0.0" # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # diff --git a/knewkarma/api.py b/knewkarma/api.py index a833722..888273e 100644 --- a/knewkarma/api.py +++ b/knewkarma/api.py @@ -10,7 +10,9 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # BASE_REDDIT_ENDPOINT: str = "https://www.reddit.com" -PYPI_PROJECT_ENDPOINT: str = "https://pypi.org/pypi/knewkarma/json" +GITHUB_RELEASE_ENDPOINT: str = ( + "https://api.github.com/repos/rly0nheart/knewkarma/releases/latest" +) # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # @@ -91,13 +93,18 @@ async def get_updates(session: aiohttp.ClientSession): :param session: aiohttp session to use for the request. """ + import rich + from rich.markdown import Markdown # Make a GET request to PyPI to get the project's latest release. - response: dict = await get_data(endpoint=PYPI_PROJECT_ENDPOINT, session=session) - release: dict = process_response(response_data=response.get("info", {})) + response: dict = await get_data(endpoint=GITHUB_RELEASE_ENDPOINT, session=session) + release: dict = process_response(response_data=response, valid_key="tag_name") if release: - remote_version: str = release.get("version") + remote_version: str = release.get("tag_name") + markup_release_notes: str = release.get("body") + markdown_release_notes = Markdown(markup=markup_release_notes) + # Splitting the version strings into components remote_parts: list = remote_version.split(".") local_parts: list = version.split(".") @@ -109,7 +116,7 @@ async def get_updates(session: aiohttp.ClientSession): # Check for differences in version parts if remote_parts[0] != local_parts[0]: update_message = ( - f"MAJOR update ({remote_version}) available:" + f"[bold][red]MAJOR[/][/] update ({remote_version}) available:" f" It might introduce significant changes." ) @@ -117,7 +124,7 @@ async def get_updates(session: aiohttp.ClientSession): elif remote_parts[1] != local_parts[1]: update_message = ( - f"MINOR update ({remote_version}) available:" + f"[bold][blue]MINOR[/][/] update ({remote_version}) available:" f" Includes small feature changes/improvements." ) @@ -125,7 +132,7 @@ async def get_updates(session: aiohttp.ClientSession): elif remote_parts[2] != local_parts[2]: update_message = ( - f"PATCH update ({remote_version}) available:" + f"[bold][green]PATCH[/][/] update ({remote_version}) available:" f" Generally for bug fixes and small tweaks." ) @@ -137,7 +144,7 @@ async def get_updates(session: aiohttp.ClientSession): and remote_parts[3] != local_parts[3] ): update_message = ( - f"BUILD update ({remote_version}) available." + f"[bold][cyan]BUILD[/][/] update ({remote_version}) available." f" Might be for specific builds or special versions." ) @@ -145,6 +152,7 @@ async def get_updates(session: aiohttp.ClientSession): if update_message: log.info(update_message) + rich.print(markdown_release_notes) # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # @@ -227,51 +235,32 @@ async def get_posts( "front_page_posts": f"{BASE_REDDIT_ENDPOINT}/.json?sort={sort}&limit={limit}&t={timeframe}", } + # ---------------------------------------------------------- # + endpoint = source_map.get(posts_type, "") if not endpoint: raise ValueError(f"Invalid profile type in {source_map}: {posts_type}") - all_posts = await paginated_posts( - posts_endpoint=endpoint, limit=limit, session=session - ) - - return all_posts[:limit] - - -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # - - -async def paginated_posts( - posts_endpoint: str, limit: int, session: aiohttp.ClientSession -) -> list: - """ - Paginates and retrieves posts until the specified limit is reached. - - :param posts_endpoint: API endpoint for retrieving posts. - :param limit: Limit of the number of posts to retrieve. - :param session: aiohttp session to use for the request. - :return: A list of all posts. - """ all_posts: list = [] last_post_id: str = "" # Determine whether to use the 'after' parameter - use_after: bool = limit > 100 + paginate_posts: bool = limit > 100 - while len(all_posts) < limit: - # ---------------------------------------------------------- # + # ---------------------------------------------------------- # + while len(all_posts) < limit: # Make the API request with the 'after' parameter if it's provided and the limit is more than 100 - if use_after and last_post_id: - endpoint_with_after: str = f"{posts_endpoint}&after={last_post_id}" + if paginate_posts and last_post_id: + pagination_endpoint: str = f"{endpoint}&after={last_post_id}" else: - endpoint_with_after: str = posts_endpoint + pagination_endpoint: str = endpoint # ---------------------------------------------------------- # raw_posts_data: dict = await get_data( - endpoint=endpoint_with_after, session=session + endpoint=pagination_endpoint, session=session ) posts_list: list = raw_posts_data.get("data", {}).get("children", []) diff --git a/knewkarma/base.py b/knewkarma/base.py index 22249ef..76f5b14 100644 --- a/knewkarma/base.py +++ b/knewkarma/base.py @@ -4,7 +4,7 @@ import aiohttp -from ._coreutils import timestamp_to_utc +from ._coreutils import unix_timestamp_to_utc from .api import get_profile, get_posts from .data import User, Subreddit, Comment, Post @@ -67,7 +67,7 @@ async def profile(self, session: aiohttp.ClientSession) -> User: awardee_karma=user_profile.get("awardee_karma"), total_karma=user_profile.get("total_karma"), subreddit=user_profile.get("subreddit"), - created_at=timestamp_to_utc(timestamp=user_profile.get("created")), + created_at=unix_timestamp_to_utc(timestamp=user_profile.get("created")), raw_data=user_profile, ) @@ -127,7 +127,7 @@ async def comments(self, session: aiohttp.ClientSession) -> List[Comment]: is_stickied=comment_data.get("stickied"), is_locked=comment_data.get("locked"), is_archived=comment_data.get("archived"), - created_at=timestamp_to_utc(timestamp=comment_data.get("created")), + created_at=unix_timestamp_to_utc(timestamp=comment_data.get("created")), subreddit=comment_data.get("subreddit_name_prefixed"), subreddit_type=comment_data.get("subreddit_type"), post_id=comment_data.get("link_id"), @@ -191,7 +191,9 @@ async def profile(self, session: aiohttp.ClientSession) -> Subreddit: current_active_users=subreddit_profile.get("accounts_active"), is_nsfw=subreddit_profile.get("over18"), language=subreddit_profile.get("lang"), - created_at=timestamp_to_utc(timestamp=subreddit_profile.get("created")), + created_at=unix_timestamp_to_utc( + timestamp=subreddit_profile.get("created") + ), raw_data=subreddit_profile, ) @@ -273,7 +275,7 @@ def process_posts(raw_posts: list) -> List[Post]: permalink=post_data.get("permalink"), is_locked=post_data.get("locked"), is_archived=post_data.get("archived"), - created_at=timestamp_to_utc(timestamp=post_data.get("created")), + created_at=unix_timestamp_to_utc(timestamp=post_data.get("created")), raw_post=post_data, ) posts_list.append(post) diff --git a/pyproject.toml b/pyproject.toml index 801b23d..26ec542 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "knewkarma" -version = "3.2.0.0" +version = "3.3.0.0" description = "A Reddit Data Analysis Toolkit." authors = ["Richard Mwewa "] readme = "README.md" @@ -31,4 +31,4 @@ rich = "*" rich-argparse = "*" [tool.poetry.scripts] -knewkarma = "knewkarma._cli:execute_cli" +knewkarma = "knewkarma._cli:execute"