huskium


Namehuskium JSON
Version 1.0.2 PyPI version JSON
download
home_pagehttps://github.com/uujohnnyuu/huskium
SummaryUI Automation Page Objects design pattern.
upload_time2025-01-21 02:57:33
maintainerNone
docs_urlNone
authorJohnny
requires_pythonNone
licenseApache 2.0
keywords huskium huskypo selenium appium page object automation
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # huskium
---

## Copyright
- Developer: Johnny Chou
---

## Overview
- **huskium** is a Page Object framework built on Selenium and Appium.
- **huskium** leverages Python’s data descriptors to simplify and enhance UI automation.
---

## Usage
- **Build page objects** simply and elegantly using the `Page` and `Element(s)` classes.
- **Write test scripts** simply and elegantly using the constructed Page objects.
---

## Page Object Example Code
1. Construct a Page object in any Python file, each page recommended to be a separate Page class.
```python
# my_page.py


from huskium import Page, Element, Elements
from huskium import By
from huskium import dynamic


class MyPage(Page):
    
    # Static element: The most common way to set up Page objects.
    # Element is a data descriptor of Page, allowing simple setup as shown below:
    search_field = Element(By.NAME, 'q', remark='Search input box')
    search_results = Elements(By.TAG_NAME, 'h3', remark='All search results')
    search_result1 = Element(By.XPATH, '(//h3)[1]', remark='First search result')
    
    # Dynamic element: Rarely used, typically determined during test case execution.
    # Must use @dynamic to enable the descriptor's functionality.
    @dynamic
    def search_result(self, order: int = 1):
        return Element(By.XPATH, f'(//h3)[{order}]', remark=f'Search result no.{order}')
    
    # For dynamic elements as properties, use the following format:
    @property
    @dynamic
    def keyword_results(self):
        return Elements(By.XPATH, f'//*[contains(text(), "{Keyword.text1}")]')
    
    # To record dynamic elements statically, use standard data descriptor dynamic assignment:
    # 1. Create a data descriptor object (e.g., static_element).
    static_element = Element()
    
    # 2. Define a function and call "dynamic" to assign a value to "static_element".
    # The logic for dynamic parameters is the same as in Element.
    # After calling "dynamic_element", you can also use "static_element" to operate it.
    def dynamic_element(self, par):
        return self.static_element.dynamic(By.XPATH, f'//*[contains(text(), "{par}")]')
    
    # 3. Use the standard method for a data descriptor.
    def dynamic_element(self, par):
        self.static_element = Element(By.XPATH, f'//*[contains(text(), "{par}")]')
        return self.static_element
```

2. After constructing the Page object, you can begin writing test cases.
```python
# test_my_page.py


from selenium import webdriver
from my_page import MyPage


class TestMyPage:
    
    driver = webdriver.Chrome()

    # Set up a page object. All actions will be triggered from this object.
    my_page = MyPage(driver)

    # The Page object can also call driver-related methods.
    my_page.get("https://google.com")

    # Example of a wait usage:
    # Wait until an element is visible, then take a screenshot.
    my_page.search_field.wait_visible()
    my_page.save_screenshot("my/file/image1.png")

    # All actions automatically handle explicit waits.
    # No need to manually call wait methods unless required, 
    # e.g. Equivalent to: 
    # my_page.search_field.wait_clickable().send_keys(keyword).wait_clickable().submit()
    my_page.search_field.send_keys(keyword).submit()

    # Various wait states are available.
    my_page.loading_image.wait_absent()
    my_page.search_results.wait_all_visible()
    my_page.save_screenshot("my/file/image2.png")

    # Assertions can be made directly:
    search_keyword = 'dinner'
    assert my_page.keyword_results(search_keyword).quantity > 1
    assert search_keyword in my_page.search_result1.text

    # Reuse found elements through existing sessions:
    # Once an element (e.g., `my_page.search_result1`) is located, 
    # it will use the same session unless the element becomes stale.
    # No need to store the found element in a separate variable.
    # Just perform actions directly:
    my_page.search_result1.click()
    ...

    driver.close()
```
---

## Timeout Global Settings
1.	In addition to setting timeouts for individual elements and methods, 
a global timeout setting is also available. See the example below:
```python
from huskium import Timeout


# Set the default timeout for all Elements.
# The huskium default is 30 seconds. You can change it as needed:
Timeout.DEFAULT = 60

# If you don’t want any waiting, you can also set it to 0:
Timeout.DEFAULT = 0

# Set the reraise behavior for timeouts on all Elements.
# The huskium default is True, with the following logic:
# True: Raise a TimeoutException if the element times out.
# False: Return False if the element times out, without raising a TimeoutException.
Timeout.RERAISE = False
```

2.	The priority order for timeout values is as follows:
- P1: Method-Level:
    - `page.element.wait_method(timeout=20)`
- P2: Element-Level:
    - `element = Element(..., timeout=10, ...)`
- P3: Global-Level:
    - `Timeout.DEFAULT = 60`

3.	The priority order for timeout reraise behavior is as follows:
- P1: Method-Level:
    - `page.element.wait_method(reraise=True)`
- P2: Global-Level:
    - `Timeout.RERAISE = False`
---

## Cache Global Settings
1. Cache determines whether the `Element` class should reference a previously located `WebElement` 
for actions or locate the element again for each action.  
2. The benefits of Cache are evident when the same `Element` is accessed multiple times, 
such as performing `.text` followed by `.click()`.
3. Note that `Elements` does not support cache. 
For multiple elements, the state can be highly unstable, 
so each action must locate the elements again to ensure expected behavior.  
```python
from huskium import Cache


# Set the default cache for all Element.
# The default is True. You can change it as needed:
Cache.ELEMENT = False


# You can also configure the cache for an individual Element:
element = Element(..., cache=False)
```

4.	The priority order for cache value is as follows:
- P1: Element-Level:
    - `element = Element(..., cache=False)`
- P2: Global-Level:
    - `Cache.ELEMENT = False`
---

## Wait Actions
We offer a comprehensive set of wait methods, 
extending the official expected_conditions in `ec_extension.py` 
and encapsulating them into corresponding methods. 
Below are the extended methods for Element(s):
```python
# Element
page.element.wait_present()
page.element.wait_absent()
page.element.wait_visible()
page.element.wait_invisible()
page.element.wait_clickable()
page.element.wait_unclickable()
page.element.wait_selected()
page.element.wait_unselected()

# Elements
page.elements.wait_all_present()
page.elements.wait_all_absent()
page.elements.wait_all_visible()
page.elements.wait_any_visible()

# You can set default timeout and reraise behavior for all wait functions.
page.element.wait_visible(timeout=10, reraise=True)
# Recommended to use default settings (timeout=30, reraise=True) for simplicity.
page.element.wait_visible()

# For reverse conditions like invisible and unclickable, 
# use the "present" parameter to define if existence is required.
# Element must be present and invisible (default).
page.element.wait_invisible(present=True)
# Element can be absent or [present and invisible].
page.element.wait_invisible(present=False)
# Element must be present and unclickable (default).
page.element.wait_unclickable(present=True)
# Element can be absent or [present and unclickable].
page.element.wait_unclickable(present=False)

# Selection states are tied to user actions, 
# so the element must be present; no "present" parameter is provided.
page.element.wait_selected()
page.element.wait_unselected()
```
---

## Appium Extended Actions
We have extended Appium with highly convenient action features, including:
```python
# Offset allows you to define swipe directions. 
# It supports eight standard directions: 
# UP, DOWN, LEFT, RIGHT, UPPER_LEFT, UPPER_RIGHT, LOWER_LEFT, LOWER_RIGHT.
# Area lets you define the swipeable range, 
# defaulting to the full device screen (FULL), or you can customize it.

from huskium import Offset, Area

# Page swiping. Refer to function docstrings for details.
page.swipe_by(Offset.UP, Area.FULL)
page.swipe_by(Offset.DOWN)
page.swipe_by(Offset.LEFT)
page.swipe_by(Offset.RIGHT)
page.swipe_by(Offset.UPPER_LEFT)
page.swipe_by(Offset.UPPER_RIGHT)
page.swipe_by(Offset.LOWER_LEFT)
page.swipe_by(Offset.LOWER_RIGHT)

# Page flicking. Refer to function docstrings for details.
# All Offset directions are supported.
page.flick_by(Offset.UP, Area.FULL)

# Element swiping until an element is visible.
# All Offset directions are supported.
page.element.swipe_by(Offset.UP, Area.FULL)

# Element flicking until an element is visible.
# All Offset directions are supported.
page.element.flick_by(Offset.UP, Area.FULL)

# Page draw lines.
# Define dots coordinates
dots = [{"x": x1, "y": y1}, {"x": x2, "y": y2}, {"x": x3, "y": y3}, ...]
# Alternatively, use element locations if available
dots = page.elements.locations
page.draw_lines(dots)

# Page draw gesture.
# 9-grid coordinates, or define your own
dots = page.elements.locations  
# 9-grid gesture string (1–9 represent grid positions). This example draws a reverse Z.
gesture = "9875321"  
page.draw_gesture(dots, gesture)
```
---

## Other Actions
All Selenium and Appium features are included. Here are some examples:
```python
# ActionChains
page.element.scroll_to_element().perform()
page.element.move_to_element().drag_and_drop().perform()

# Temporarily store ActionChains and execute later
page.element.move_to_element().drag_and_drop()
...  # Process other logic before executing perform()
page.element.perform()

# Select options
page.element.options
page.element.select_by_value()
```
---

## Logstack
Using logstack to log specific frame information.
The logstack module extends logging functionality, 
allowing you to capture information for specific frames, 
such as those starting with a designated prefix (e.g., test), 
without tracing all frames manually.
```python
from huskium import logstack

# Configure logging using either logging.basicConfig() or logstack.config().
# logstack.config() simplifies the default settings. You can use it as shown below
# to output the log file to "./log.log".
logstack.config()

# Use logstack in your code to log specific frames
def some_func():
    ...
    # Logs information from the first frame with the prefix (default: test)
    logstack.info("Log from some function.", prefix="test")

def test_func():
    ...
    # Logs frame info for test_func, not some_func
    some_func()

# Example log output:
# 2025-01-04 18:20:48 | INFO | testing.py:32 | test_func | Log from some function.
```

## Inheritance
You can also extend the Page and Element classes to include custom methods. 
There’s no need to manually handle descriptors, and the inheritance usage remains unchanged.
```python
from huskium import Page as HuskyPage
from huskium import Element as HuskyElement


class Page(HuskyPage):

    def extended_func(self, par):
        ...


class Element(HuskyElement):

    def extended_func(self, par):
        ...
```
---

## TODO
Keep tracking the Appium version.  

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/uujohnnyuu/huskium",
    "name": "huskium",
    "maintainer": null,
    "docs_url": null,
    "requires_python": null,
    "maintainer_email": null,
    "keywords": "huskium, huskypo, selenium, appium, page object, automation",
    "author": "Johnny",
    "author_email": "johnny071531@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/4b/6d/8fe33a3c392cfde5d05bbafb409ca494971bb9f4050e19b79fd5384ad23b/huskium-1.0.2.tar.gz",
    "platform": null,
    "description": "# huskium\n---\n\n## Copyright\n- Developer: Johnny Chou\n---\n\n## Overview\n- **huskium** is a Page Object framework built on Selenium and Appium.\n- **huskium** leverages Python\u2019s data descriptors to simplify and enhance UI automation.\n---\n\n## Usage\n- **Build page objects** simply and elegantly using the `Page` and `Element(s)` classes.\n- **Write test scripts** simply and elegantly using the constructed Page objects.\n---\n\n## Page Object Example Code\n1. Construct a Page object in any Python file, each page recommended to be a separate Page class.\n```python\n# my_page.py\n\n\nfrom huskium import Page, Element, Elements\nfrom huskium import By\nfrom huskium import dynamic\n\n\nclass MyPage(Page):\n    \n    # Static element: The most common way to set up Page objects.\n    # Element is a data descriptor of Page, allowing simple setup as shown below:\n    search_field = Element(By.NAME, 'q', remark='Search input box')\n    search_results = Elements(By.TAG_NAME, 'h3', remark='All search results')\n    search_result1 = Element(By.XPATH, '(//h3)[1]', remark='First search result')\n    \n    # Dynamic element: Rarely used, typically determined during test case execution.\n    # Must use @dynamic to enable the descriptor's functionality.\n    @dynamic\n    def search_result(self, order: int = 1):\n        return Element(By.XPATH, f'(//h3)[{order}]', remark=f'Search result no.{order}')\n    \n    # For dynamic elements as properties, use the following format:\n    @property\n    @dynamic\n    def keyword_results(self):\n        return Elements(By.XPATH, f'//*[contains(text(), \"{Keyword.text1}\")]')\n    \n    # To record dynamic elements statically, use standard data descriptor dynamic assignment:\n    # 1. Create a data descriptor object (e.g., static_element).\n    static_element = Element()\n    \n    # 2. Define a function and call \"dynamic\" to assign a value to \"static_element\".\n    # The logic for dynamic parameters is the same as in Element.\n    # After calling \"dynamic_element\", you can also use \"static_element\" to operate it.\n    def dynamic_element(self, par):\n        return self.static_element.dynamic(By.XPATH, f'//*[contains(text(), \"{par}\")]')\n    \n    # 3. Use the standard method for a data descriptor.\n    def dynamic_element(self, par):\n        self.static_element = Element(By.XPATH, f'//*[contains(text(), \"{par}\")]')\n        return self.static_element\n```\n\n2. After constructing the Page object, you can begin writing test cases.\n```python\n# test_my_page.py\n\n\nfrom selenium import webdriver\nfrom my_page import MyPage\n\n\nclass TestMyPage:\n    \n    driver = webdriver.Chrome()\n\n    # Set up a page object. All actions will be triggered from this object.\n    my_page = MyPage(driver)\n\n    # The Page object can also call driver-related methods.\n    my_page.get(\"https://google.com\")\n\n    # Example of a wait usage:\n    # Wait until an element is visible, then take a screenshot.\n    my_page.search_field.wait_visible()\n    my_page.save_screenshot(\"my/file/image1.png\")\n\n    # All actions automatically handle explicit waits.\n    # No need to manually call wait methods unless required, \n    # e.g. Equivalent to: \n    # my_page.search_field.wait_clickable().send_keys(keyword).wait_clickable().submit()\n    my_page.search_field.send_keys(keyword).submit()\n\n    # Various wait states are available.\n    my_page.loading_image.wait_absent()\n    my_page.search_results.wait_all_visible()\n    my_page.save_screenshot(\"my/file/image2.png\")\n\n    # Assertions can be made directly:\n    search_keyword = 'dinner'\n    assert my_page.keyword_results(search_keyword).quantity > 1\n    assert search_keyword in my_page.search_result1.text\n\n    # Reuse found elements through existing sessions:\n    # Once an element (e.g., `my_page.search_result1`) is located, \n    # it will use the same session unless the element becomes stale.\n    # No need to store the found element in a separate variable.\n    # Just perform actions directly:\n    my_page.search_result1.click()\n    ...\n\n    driver.close()\n```\n---\n\n## Timeout Global Settings\n1.\tIn addition to setting timeouts for individual elements and methods, \na global timeout setting is also available. See the example below:\n```python\nfrom huskium import Timeout\n\n\n# Set the default timeout for all Elements.\n# The huskium default is 30 seconds. You can change it as needed:\nTimeout.DEFAULT = 60\n\n# If you don\u2019t want any waiting, you can also set it to 0:\nTimeout.DEFAULT = 0\n\n# Set the reraise behavior for timeouts on all Elements.\n# The huskium default is True, with the following logic:\n# True: Raise a TimeoutException if the element times out.\n# False: Return False if the element times out, without raising a TimeoutException.\nTimeout.RERAISE = False\n```\n\n2.\tThe priority order for timeout values is as follows:\n- P1: Method-Level:\n    - `page.element.wait_method(timeout=20)`\n- P2: Element-Level:\n    - `element = Element(..., timeout=10, ...)`\n- P3: Global-Level:\n    - `Timeout.DEFAULT = 60`\n\n3.\tThe priority order for timeout reraise behavior is as follows:\n- P1: Method-Level:\n    - `page.element.wait_method(reraise=True)`\n- P2: Global-Level:\n    - `Timeout.RERAISE = False`\n---\n\n## Cache Global Settings\n1. Cache determines whether the `Element` class should reference a previously located `WebElement` \nfor actions or locate the element again for each action.  \n2. The benefits of Cache are evident when the same `Element` is accessed multiple times, \nsuch as performing `.text` followed by `.click()`.\n3. Note that `Elements` does not support cache. \nFor multiple elements, the state can be highly unstable, \nso each action must locate the elements again to ensure expected behavior.  \n```python\nfrom huskium import Cache\n\n\n# Set the default cache for all Element.\n# The default is True. You can change it as needed:\nCache.ELEMENT = False\n\n\n# You can also configure the cache for an individual Element:\nelement = Element(..., cache=False)\n```\n\n4.\tThe priority order for cache value is as follows:\n- P1: Element-Level:\n    - `element = Element(..., cache=False)`\n- P2: Global-Level:\n    - `Cache.ELEMENT = False`\n---\n\n## Wait Actions\nWe offer a comprehensive set of wait methods, \nextending the official expected_conditions in `ec_extension.py` \nand encapsulating them into corresponding methods. \nBelow are the extended methods for Element(s):\n```python\n# Element\npage.element.wait_present()\npage.element.wait_absent()\npage.element.wait_visible()\npage.element.wait_invisible()\npage.element.wait_clickable()\npage.element.wait_unclickable()\npage.element.wait_selected()\npage.element.wait_unselected()\n\n# Elements\npage.elements.wait_all_present()\npage.elements.wait_all_absent()\npage.elements.wait_all_visible()\npage.elements.wait_any_visible()\n\n# You can set default timeout and reraise behavior for all wait functions.\npage.element.wait_visible(timeout=10, reraise=True)\n# Recommended to use default settings (timeout=30, reraise=True) for simplicity.\npage.element.wait_visible()\n\n# For reverse conditions like invisible and unclickable, \n# use the \"present\" parameter to define if existence is required.\n# Element must be present and invisible (default).\npage.element.wait_invisible(present=True)\n# Element can be absent or [present and invisible].\npage.element.wait_invisible(present=False)\n# Element must be present and unclickable (default).\npage.element.wait_unclickable(present=True)\n# Element can be absent or [present and unclickable].\npage.element.wait_unclickable(present=False)\n\n# Selection states are tied to user actions, \n# so the element must be present; no \"present\" parameter is provided.\npage.element.wait_selected()\npage.element.wait_unselected()\n```\n---\n\n## Appium Extended Actions\nWe have extended Appium with highly convenient action features, including:\n```python\n# Offset allows you to define swipe directions. \n# It supports eight standard directions: \n# UP, DOWN, LEFT, RIGHT, UPPER_LEFT, UPPER_RIGHT, LOWER_LEFT, LOWER_RIGHT.\n# Area lets you define the swipeable range, \n# defaulting to the full device screen (FULL), or you can customize it.\n\nfrom huskium import Offset, Area\n\n# Page swiping. Refer to function docstrings for details.\npage.swipe_by(Offset.UP, Area.FULL)\npage.swipe_by(Offset.DOWN)\npage.swipe_by(Offset.LEFT)\npage.swipe_by(Offset.RIGHT)\npage.swipe_by(Offset.UPPER_LEFT)\npage.swipe_by(Offset.UPPER_RIGHT)\npage.swipe_by(Offset.LOWER_LEFT)\npage.swipe_by(Offset.LOWER_RIGHT)\n\n# Page flicking. Refer to function docstrings for details.\n# All Offset directions are supported.\npage.flick_by(Offset.UP, Area.FULL)\n\n# Element swiping until an element is visible.\n# All Offset directions are supported.\npage.element.swipe_by(Offset.UP, Area.FULL)\n\n# Element flicking until an element is visible.\n# All Offset directions are supported.\npage.element.flick_by(Offset.UP, Area.FULL)\n\n# Page draw lines.\n# Define dots coordinates\ndots = [{\"x\": x1, \"y\": y1}, {\"x\": x2, \"y\": y2}, {\"x\": x3, \"y\": y3}, ...]\n# Alternatively, use element locations if available\ndots = page.elements.locations\npage.draw_lines(dots)\n\n# Page draw gesture.\n# 9-grid coordinates, or define your own\ndots = page.elements.locations  \n# 9-grid gesture string (1\u20139 represent grid positions). This example draws a reverse Z.\ngesture = \"9875321\"  \npage.draw_gesture(dots, gesture)\n```\n---\n\n## Other Actions\nAll Selenium and Appium features are included. Here are some examples:\n```python\n# ActionChains\npage.element.scroll_to_element().perform()\npage.element.move_to_element().drag_and_drop().perform()\n\n# Temporarily store ActionChains and execute later\npage.element.move_to_element().drag_and_drop()\n...  # Process other logic before executing perform()\npage.element.perform()\n\n# Select options\npage.element.options\npage.element.select_by_value()\n```\n---\n\n## Logstack\nUsing logstack to log specific frame information.\nThe logstack module extends logging functionality, \nallowing you to capture information for specific frames, \nsuch as those starting with a designated prefix (e.g., test), \nwithout tracing all frames manually.\n```python\nfrom huskium import logstack\n\n# Configure logging using either logging.basicConfig() or logstack.config().\n# logstack.config() simplifies the default settings. You can use it as shown below\n# to output the log file to \"./log.log\".\nlogstack.config()\n\n# Use logstack in your code to log specific frames\ndef some_func():\n    ...\n    # Logs information from the first frame with the prefix (default: test)\n    logstack.info(\"Log from some function.\", prefix=\"test\")\n\ndef test_func():\n    ...\n    # Logs frame info for test_func, not some_func\n    some_func()\n\n# Example log output:\n# 2025-01-04 18:20:48 | INFO | testing.py:32 | test_func | Log from some function.\n```\n\n## Inheritance\nYou can also extend the Page and Element classes to include custom methods. \nThere\u2019s no need to manually handle descriptors, and the inheritance usage remains unchanged.\n```python\nfrom huskium import Page as HuskyPage\nfrom huskium import Element as HuskyElement\n\n\nclass Page(HuskyPage):\n\n    def extended_func(self, par):\n        ...\n\n\nclass Element(HuskyElement):\n\n    def extended_func(self, par):\n        ...\n```\n---\n\n## TODO\nKeep tracking the Appium version.  \n",
    "bugtrack_url": null,
    "license": "Apache 2.0",
    "summary": "UI Automation Page Objects design pattern.",
    "version": "1.0.2",
    "project_urls": {
        "Homepage": "https://github.com/uujohnnyuu/huskium"
    },
    "split_keywords": [
        "huskium",
        " huskypo",
        " selenium",
        " appium",
        " page object",
        " automation"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "95050449c74b2f8c669c2e22b3e26e5f06b550b187e8a7649f136c480bb5796f",
                "md5": "01acd1e882b5b658338cd6f1c58379e4",
                "sha256": "112ff46013c11286bba14f47a16f8c46aa4a8c200b52a6c74a842019ea617f53"
            },
            "downloads": -1,
            "filename": "huskium-1.0.2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "01acd1e882b5b658338cd6f1c58379e4",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": null,
            "size": 45572,
            "upload_time": "2025-01-21T02:57:30",
            "upload_time_iso_8601": "2025-01-21T02:57:30.630685Z",
            "url": "https://files.pythonhosted.org/packages/95/05/0449c74b2f8c669c2e22b3e26e5f06b550b187e8a7649f136c480bb5796f/huskium-1.0.2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "4b6d8fe33a3c392cfde5d05bbafb409ca494971bb9f4050e19b79fd5384ad23b",
                "md5": "e983e8f04997b617c2ef47f1c3c6881e",
                "sha256": "9670aa463bb7e0169d918d6dc3854c0aea20f920ed5d4d5d53a69ae9e597890d"
            },
            "downloads": -1,
            "filename": "huskium-1.0.2.tar.gz",
            "has_sig": false,
            "md5_digest": "e983e8f04997b617c2ef47f1c3c6881e",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 45751,
            "upload_time": "2025-01-21T02:57:33",
            "upload_time_iso_8601": "2025-01-21T02:57:33.638065Z",
            "url": "https://files.pythonhosted.org/packages/4b/6d/8fe33a3c392cfde5d05bbafb409ca494971bb9f4050e19b79fd5384ad23b/huskium-1.0.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-01-21 02:57:33",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "uujohnnyuu",
    "github_project": "huskium",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "huskium"
}
        
Elapsed time: 0.41220s