|
1 | 1 | """ |
2 | 2 | Created on 2023-12-14 |
3 | 3 |
|
4 | | -This module, developed as part of the ngwidgets package under the instruction of WF, provides |
5 | | -classes and methods for interacting with the Python Package Index (PyPI). It includes the |
6 | | -`Project` data class for representing software projects and the `PyPi` class for searching |
7 | | -and retrieving package information from PyPI. The code facilitates the creation of tools and |
| 4 | +This module, developed as part of the ngwidgets package under the instruction of WF, provides |
| 5 | +classes and methods for interacting with the Python Package Index (PyPI). It includes the |
| 6 | +`Project` data class for representing software projects and the `PyPi` class for searching |
| 7 | +and retrieving package information from PyPI. The code facilitates the creation of tools and |
8 | 8 | applications that interact with PyPI for information about Python packages. |
9 | 9 |
|
10 | | -Prompts for LLM: |
| 10 | +Prompts for LLM: |
11 | 11 | - Create Python classes Project and Projects (holding a list of Project elements) for interacting with PyPI and github, including search functionality. |
12 | 12 | - Develop a data class in Python to represent a software project with the attributes. |
13 | 13 | name (str): The name of the project. |
|
28 | 28 | downloads (int): Number of downloads from PyPI. |
29 | 29 | categories (List[str]): Categories associated with the project. |
30 | 30 | version (str): The current version of the project on PyPI. |
31 | | - |
| 31 | +
|
32 | 32 | - Implement methods to search PyPI and github for packages/repos that represent projects and retrieve detailed package information on a given topic. |
33 | 33 | - allow saving and loading the collected projects |
34 | 34 |
|
@@ -649,108 +649,82 @@ class PyPi: |
649 | 649 | def __init__(self, debug: bool = False): |
650 | 650 | self.base_url = "https://pypi.org/pypi" |
651 | 651 | self.debug = debug |
| 652 | + self.cache_dir = Path.home() / ".pypi" |
| 653 | + self.cache_dir.mkdir(exist_ok=True) |
| 654 | + self.cache_file = self.cache_dir / "pypi-package-list.json" |
| 655 | + self.cache_ttl = 3600 # 1 hour |
| 656 | + |
| 657 | + |
| 658 | + def pypi_request(self, url: str, headers: Optional[Dict] = None) -> Optional[Dict]: |
| 659 | + """Make pypi request with simple file caching.""" |
| 660 | + data = None |
| 661 | + if self.cache_file.exists(): |
| 662 | + file_age = time.time() - self.cache_file.stat().st_mtime |
| 663 | + if file_age < self.cache_ttl: |
| 664 | + try: |
| 665 | + with open(self.cache_file) as f: |
| 666 | + data = json.load(f) |
| 667 | + except json.JSONDecodeError: |
| 668 | + pass |
| 669 | + |
| 670 | + if not data: |
| 671 | + data = self._make_request(url, headers) |
| 672 | + if data: |
| 673 | + with open(self.cache_file, "w") as f: |
| 674 | + json.dump(data, f) |
| 675 | + |
| 676 | + return data |
| 677 | + |
| 678 | + def _make_request(self, url: str, headers: Optional[Dict] = None) -> Optional[Dict]: |
| 679 | + """Make a request to PyPI API and return JSON response.""" |
| 680 | + try: |
| 681 | + request = urllib.request.Request(url, headers=headers if headers else {}) |
| 682 | + response = urllib.request.urlopen(request) |
| 683 | + if response.getcode() == 200: |
| 684 | + json_response = json.loads(response.read()) |
| 685 | + return json_response |
| 686 | + except (urllib.error.URLError, json.JSONDecodeError) as e: |
| 687 | + if self.debug: |
| 688 | + print(f"Error in request to {url}: {e}") |
| 689 | + return None |
652 | 690 |
|
653 | | - def search_projects(self, term: str, limit: int = None) -> List[Project]: |
654 | | - """ |
655 | | - Search for packages on PyPI and return them as Project instances. |
656 | | -
|
657 | | - Args: |
658 | | - term (str): The search term. |
659 | | - limit (int, optional): Maximum number of results to return. |
660 | | -
|
661 | | - Returns: |
662 | | - List[Project]: A list of Project instances representing the search results. |
663 | | - """ |
664 | | - package_dicts = self.search_packages(term, limit) |
665 | | - return [Project.from_pypi(pkg) for pkg in package_dicts] |
666 | | - |
667 | | - def get_package_info(self, package_name: str) -> dict: |
668 | | - """ |
669 | | - Get detailed information about a package from PyPI using urllib. |
670 | | -
|
671 | | - Args: |
672 | | - package_name (str): The name of the package to retrieve information for. |
673 | | -
|
674 | | - Returns: |
675 | | - dict: A dictionary containing package information. |
676 | 691 |
|
677 | | - Raises: |
678 | | - urllib.error.URLError: If there is an issue with the URL. |
679 | | - ValueError: If the response status code is not 200. |
680 | | - """ |
| 692 | + def get_package_info(self, package_name: str) -> Optional[Dict]: |
| 693 | + """Get detailed info for a specific package.""" |
681 | 694 | url = f"{self.base_url}/{package_name}/json" |
682 | | - |
683 | | - response = urllib.request.urlopen(url) |
684 | | - |
685 | | - if response.getcode() != 200: |
686 | | - raise ValueError( |
687 | | - f"Failed to fetch package info for {package_name}. Status code: {response.getcode()}" |
688 | | - ) |
689 | | - |
690 | | - package_data = json.loads(response.read()) |
691 | | - |
| 695 | + package_data = self._make_request(url) |
| 696 | + if package_data: |
| 697 | + package_data['package_url'] = f"https://pypi.org/project/{package_name}" |
692 | 698 | return package_data |
693 | 699 |
|
694 | | - def search_packages(self, term: str, limit: int = None) -> list: |
695 | | - """Search a package in the pypi repositories and retrieve detailed package information. |
696 | | -
|
697 | | - Args: |
698 | | - term (str): The search term. |
699 | | - limit (int, optional): Maximum number of results to return. |
700 | | -
|
701 | | - Returns: |
702 | | - List[Dict]: A list of dictionaries containing detailed package information. |
703 | | -
|
704 | | - see https://raw.githubusercontent.com/shubhodeep9/pipsearch/master/pipsearch/api.py |
705 | | - """ |
706 | | - # Constructing a search URL and sending the request |
707 | | - url = "https://pypi.org/search/?q=" + term |
708 | | - try: |
709 | | - response = urllib.request.urlopen(url) |
710 | | - text = response.read() |
711 | | - except Exception as e: |
712 | | - raise e |
713 | | - |
714 | | - soup = BeautifulSoup(text, "html.parser") |
715 | | - packagestable = soup.find("ul", {"class": "unstyled"}) |
716 | | - # Constructing the result list |
| 700 | + def search_packages(self, term: str, limit: Optional[int] = None) -> List[Dict]: |
| 701 | + """Search packages using PyPI JSON API.""" |
| 702 | + headers = {"Accept": "application/vnd.pypi.simple.v1+json"} |
| 703 | + url = "https://pypi.org/simple/" |
717 | 704 | packages = [] |
718 | 705 |
|
719 | | - # If no package exists then there is no table displayed hence soup.table will be None |
720 | | - if packagestable is None: |
721 | | - return packages |
| 706 | + projects = self.pypi_request(url, headers) |
| 707 | + if projects: |
| 708 | + matched_projects = [ |
| 709 | + p for p in projects['projects'] |
| 710 | + if term.lower() in p['name'].lower() |
| 711 | + ] |
| 712 | + if limit: |
| 713 | + matched_projects = matched_projects[:limit] |
722 | 714 |
|
723 | | - packagerows: ResultSet[Tag] = packagestable.findAll("li") |
| 715 | + for project in matched_projects: |
| 716 | + package_info = self.get_package_info(project['name']) |
| 717 | + if package_info: |
| 718 | + packages.append(package_info) |
724 | 719 |
|
725 | | - if self.debug: |
726 | | - print(f"found len{packagerows} package rows") |
727 | | - if limit: |
728 | | - selected_rows = packagerows[:limit] |
729 | | - else: |
730 | | - selected_rows = packagerows |
731 | | - for package in selected_rows: |
732 | | - nameSelector = package.find("span", {"class": "package-snippet__name"}) |
733 | | - if nameSelector is None: |
734 | | - continue |
735 | | - name = nameSelector.text |
736 | | - |
737 | | - link = "" |
738 | | - if package.a is not None: |
739 | | - href = package.a["href"] |
740 | | - if isinstance(href, list): |
741 | | - href = href[0] |
742 | | - link = "https://pypi.org" + href |
743 | | - |
744 | | - description = ( |
745 | | - package.find("p", {"class": "package-snippet__description"}) or Tag() |
746 | | - ).text |
747 | | - |
748 | | - version = ( |
749 | | - package.find("span", {"class": "package-snippet__version"}) or Tag() |
750 | | - ).text |
751 | | - package_info = self.get_package_info(name) |
752 | | - package_info["package_url"] = link |
753 | | - packages.append(package_info) |
754 | | - |
755 | | - # returning the result list back |
756 | 720 | return packages |
| 721 | + |
| 722 | + def search_projects(self, term: str, limit: Optional[int] = None) -> List[Project]: |
| 723 | + """Get PyPI package info as Project instances.""" |
| 724 | + package_dicts = self.search_packages(term, limit) |
| 725 | + projects = [] |
| 726 | + for package in package_dicts: |
| 727 | + if package: |
| 728 | + project = Project.from_pypi(package) |
| 729 | + projects.append(project) |
| 730 | + return projects |
0 commit comments