├── .python-version ├── screenshot ├── osilic keyword.png ├── osilic steward.png ├── osilic_all_licenses.png ├── osilic_lic_details.png ├── osilic_lic_search.png ├── osilic_missing_lic.png ├── osilic keyword suggest.png └── osilic steward suggest.png ├── .gitignore ├── Makefile ├── pyproject.toml ├── LICENSE ├── CHANGELOG.md ├── README.md └── src └── osilic ├── __init__.py └── model.py /.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /screenshot/osilic keyword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TH/osilic/main/screenshot/osilic keyword.png -------------------------------------------------------------------------------- /screenshot/osilic steward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TH/osilic/main/screenshot/osilic steward.png -------------------------------------------------------------------------------- /screenshot/osilic_all_licenses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TH/osilic/main/screenshot/osilic_all_licenses.png -------------------------------------------------------------------------------- /screenshot/osilic_lic_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TH/osilic/main/screenshot/osilic_lic_details.png -------------------------------------------------------------------------------- /screenshot/osilic_lic_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TH/osilic/main/screenshot/osilic_lic_search.png -------------------------------------------------------------------------------- /screenshot/osilic_missing_lic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TH/osilic/main/screenshot/osilic_missing_lic.png -------------------------------------------------------------------------------- /screenshot/osilic keyword suggest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TH/osilic/main/screenshot/osilic keyword suggest.png -------------------------------------------------------------------------------- /screenshot/osilic steward suggest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TH/osilic/main/screenshot/osilic steward suggest.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__ 3 | __pycache__/ 4 | *.py[oc] 5 | build/ 6 | dist/ 7 | wheels/ 8 | *.egg-info 9 | o.txt 10 | license.json 11 | 12 | # Virtual environments 13 | .venv 14 | .codesight 15 | .vscode -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: changelog build install clean 2 | 3 | changelog: 4 | git log --pretty=format:'- %h %s (%an, %ad)' --date=short > CHANGELOG.md 5 | 6 | build: 7 | uv build 8 | 9 | install: 10 | pip install -e . 11 | 12 | clean: 13 | rm -rf build dist *.egg-info 14 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "osilic" 3 | version = "0.2.2" 4 | description = "An utility for listing & searching Open Source Initiative licenses via their public API" 5 | readme = "README.md" 6 | authors = [{ name = "Dinesh", email = "dineshr93@gmail.com" }] 7 | requires-python = ">=3.10" 8 | dependencies = ["tabulate>=0.9.0", "requests>=2.3.0"] 9 | 10 | [project.scripts] 11 | osilic = "osilic:main" 12 | 13 | [build-system] 14 | requires = ["hatchling"] 15 | build-backend = "hatchling.build" 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0) 2 | 3 | Copyright © 2025 Dinesh R 4 | 5 | You are free to: 6 | - Share — copy and redistribute the material in any medium or format 7 | - Adapt — remix, transform, and build upon the material for any purpose, even commercially. 8 | 9 | Under the following terms: 10 | - Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. 11 | - ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original. 12 | 13 | Full license text: https://creativecommons.org/licenses/by-sa/3.0/legalcode 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | - f2c4cbf Update README.md (Dinesh Ravi, 2025-07-14) 2 | - 2bff2ec add new screenshots (Dinesh Ravi, 2025-07-14) 3 | - ca272df fix stewards no attribute value (Dinesh Ravi, 2025-07-14) 4 | - 49786d0 fix missing print_licenses_table_with_steward (Dinesh Ravi, 2025-07-14) 5 | - c4612dd add print_licenses_table_with_steward (Dinesh Ravi, 2025-07-14) 6 | - 16ed910 remove the debug (Dinesh Ravi, 2025-07-14) 7 | - ca0c062 add enum value (Dinesh Ravi, 2025-07-14) 8 | - 1d79f6f use set add (Dinesh Ravi, 2025-07-14) 9 | - 0419c54 fix str(keyword) (Dinesh Ravi, 2025-07-14) 10 | - 6afe6cc debug keyword (Dinesh Ravi, 2025-07-14) 11 | - b706f90 fix enum keyword (Dinesh Ravi, 2025-07-14) 12 | - 3871702 fix keyword object not iterable (Dinesh Ravi, 2025-07-14) 13 | - 511d630 revert enum (Dinesh Ravi, 2025-07-14) 14 | - a747612 Unpack enum keywords (Dinesh Ravi, 2025-07-14) 15 | - 0b27a51 fix case sensitive keyword option (Dinesh Ravi, 2025-07-14) 16 | - 27ddad0 fix elif (Dinesh Ravi, 2025-07-14) 17 | - 311aa2b fix keyword case (Dinesh Ravi, 2025-07-14) 18 | - 05c1425 fix unpaccking variable (Dinesh Ravi, 2025-07-14) 19 | - cbf6c64 added stewards and keywords (Dinesh Ravi, 2025-07-14) 20 | - 1240891 Add description in toml (Dinesh, 2025-07-14) 21 | - db1a396 fix source link in Readme (Dinesh, 2025-07-14) 22 | - 4bee5fb Update pyproject.toml (Dinesh Ravi, 2025-07-14) 23 | - f60ba74 Update README.md (Dinesh Ravi, 2025-07-14) 24 | - b3b4d68 Update pyproject.toml (Dinesh Ravi, 2025-07-14) 25 | - ccdadff Update README.md (Dinesh Ravi, 2025-07-14) 26 | - e5c249e update license link in readme (Dinesh Ravi, 2025-07-14) 27 | - 4f73381 Update pyproject.toml (Dinesh Ravi, 2025-07-14) 28 | - 0107c2c Update README.md (Dinesh Ravi, 2025-07-14) 29 | - 4b1d89c update to 0.1.1 (Dinesh Ravi, 2025-07-14) 30 | - c180117 Update README.md (Dinesh Ravi, 2025-07-14) 31 | - e905a5a updated readme (Dinesh, 2025-07-14) 32 | - 5a9d3fa all license screenshot (Dinesh, 2025-07-14) 33 | - d2a2d99 improve change log (Dinesh, 2025-07-14) 34 | - 5568ec7 add screenshots (Dinesh, 2025-07-14) 35 | - 6227d95 update changelog (Dinesh, 2025-07-14) 36 | - f943e59 first commit (Dinesh, 2025-07-14) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # osilic: OSI License CLI & Python Package 2 | 3 | osilic is a Python package and command-line tool for listing, searching, and viewing details of OSI Approved Licenses® using the [official OSI API](https://opensource.org/blog/introducing-the-new-api-for-osi-approved-licenses). 4 | 5 | ## Features 6 | - List all OSI Approved Licenses® 7 | - View details for a specific license by SPDX ID 8 | - Search licenses by name 9 | - Automatic suggestions for similar licenses if a license is not found 10 | - Usable as a standalone CLI or as a Python package 11 | - Filter licenses by keywords 12 | - Auto suggest available keywords upon entering wrong keyword filters 13 | - Filter licenses by stewards 14 | - Auto suggest available licenses upon entering wrong stewards filters 15 | 16 | ## Installation 17 | 18 | ### From Pypi index 19 | ```bash 20 | pip install osilic 21 | ``` 22 | 23 | ### Using pip (editable mode for development) 24 | ```bash 25 | pip install -e . 26 | ``` 27 | Or with [uv](https://github.com/astral-sh/uv): 28 | ```bash 29 | uv pip install -e . 30 | ``` 31 | 32 | ### Requirements 33 | - Python 3.8+ 34 | - `requests` and `tabulate` Python packages 35 | 36 | ## Usage 37 | 38 | ### CLI 39 | After installation, use the `osilic` command: 40 | 41 | - List all licenses: 42 | ```bash 43 | osilic 44 | ``` 45 | ![osilic](https://raw.githubusercontent.com/dineshr93/osilic/refs/heads/main/screenshot/osilic_all_licenses.png) 46 | 47 | - Show details for a license by SPDX ID: 48 | ```bash 49 | osilic gpl-2-0 50 | ``` 51 | ![osilic gpl-3-0](https://raw.githubusercontent.com/dineshr93/osilic/refs/heads/main/screenshot/osilic_lic_details.png) 52 | 53 | - Search licenses by name (-s): 54 | ```bash 55 | osilic -s mit 56 | ``` 57 | ![osilic -s gpl](https://raw.githubusercontent.com/dineshr93/osilic/refs/heads/main/screenshot/osilic_lic_search.png) 58 | 59 | - If a license is not found, the CLI will suggest similar licenses automatically. 60 | ![osilic mi](https://raw.githubusercontent.com/dineshr93/osilic/refs/heads/main/screenshot/osilic_missing_lic.png) 61 | 62 | - Filter licenses by steward (-w): 63 | ```bash 64 | osilic -w zope-foundation 65 | ``` 66 | ![osilic -w zope-foundationl](https://raw.githubusercontent.com/dineshr93/osilic/refs/heads/main/screenshot/osilic%20steward.png) 67 | 68 | - If a license is not found, the CLI will suggest list of stewards to use automatically. 69 | ![osilic -w dummy](https://raw.githubusercontent.com/dineshr93/osilic/refs/heads/main/screenshot/osilic%20steward%20suggest.png) 70 | 71 | - Filter licenses by keyword (-k): 72 | ```bash 73 | osilic -k other-miscellaneous 74 | ``` 75 | ![osilic -k other-miscellaneous](https://raw.githubusercontent.com/dineshr93/osilic/refs/heads/main/screenshot/osilic%20keyword.png) 76 | 77 | - If a license is not found, the CLI will suggest list of keywords to use automatically. 78 | ![osilic -k test](https://raw.githubusercontent.com/dineshr93/osilic/refs/heads/main/screenshot/osilic%20keyword%20suggest.png) 79 | 80 | ### As a Python Package 81 | You can also use OLC in your own Python code: 82 | ```python 83 | from olc.model import license_from_dict, print_licenses_table, print_license_details_table 84 | import requests 85 | 86 | resp = requests.get("https://opensource.org/api/license") 87 | licenses = license_from_dict(resp.json()) 88 | print_licenses_table(licenses) 89 | ``` 90 | 91 | ## API Reference 92 | - List all licenses: `https://opensource.org/api/license` 93 | - License details: `https://opensource.org/api/license/{spdx-id}` 94 | - Search licenses: `https://opensource.org/api/license?name={search_key}` 95 | - Filter licenses by keywords: `https://opensource.org/api/license?keyword={filter_keyword}` 96 | - Filter licenses by stewards: `https://opensource.org/api/license?steward={filter_steward_key}` 97 | 98 | ## Reference & Further Reading 99 | - Official OSI API Blog Post: [Introducing the New API for OSI Approved Licenses](https://opensource.org/blog/introducing-the-new-api-for-osi-approved-licenses) 100 | - For more information on OSI licenses, visit [opensource.org](https://opensource.org/licenses). 101 | 102 | ## Development 103 | - Source code: [GitHub](https://github.com/dineshr93/osilic) 104 | - Discusstion: [OSI discussion](https://discuss.opensource.org/t/introducing-the-new-api-for-osi-approved-licenses/1169/1) 105 | - Issues and contributions welcome! 106 | 107 | ## License 108 | This project is licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0) License. 109 | 110 | Copyright © 2025 Dinesh R 111 | 112 | See [LICENSE](https://github.com/dineshr93/osilic/blob/main/LICENSE) for details. 113 | 114 | ## Author 115 | - Dinesh R 116 | 117 | ## Changelog 118 | See [CHANGELOG.md](https://github.com/dineshr93/osilic/blob/main/CHANGELOG.md) for a list of all commits and changes. 119 | 120 | --- 121 | For more information on OSI licenses, visit [opensource.org](https://opensource.org/licenses). 122 | -------------------------------------------------------------------------------- /src/osilic/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import requests 3 | import argparse 4 | from osilic.model import license_from_dict, print_licenses_table, print_license_details_table, print_licenses_table_with_steward 5 | 6 | def main() -> None: 7 | parser = argparse.ArgumentParser( 8 | prog="osilic", 9 | description="OSI License CLI: List, search, and show license details." 10 | ) 11 | parser.add_argument("spdx_id", nargs="?", help="SPDX ID of the license to show details for.") 12 | parser.add_argument("-s", "--search", metavar="SEARCH_KEY", help="Search licenses by name.") 13 | parser.add_argument("-w", "--steward", metavar="STEWARD_KEY", help="Search licenses by steward.") 14 | parser.add_argument("-k", "--keyword", metavar="FILTER_KEY", help="Filter licenses by keyword.") 15 | args = parser.parse_args() 16 | 17 | base_url = "https://opensource.org/api/license" 18 | 19 | if args.keyword: 20 | # Search licenses by steward 21 | resp = requests.get(f"{base_url}?keyword={args.keyword}") 22 | if resp.status_code == 200: 23 | licenses = license_from_dict(resp.json()) 24 | if len(licenses)>0: 25 | print_licenses_table(licenses) 26 | else: 27 | print("No licenses found for keyword:", args.keyword) 28 | # Get all licenses and then stewards for display 29 | resp = requests.get(base_url) 30 | unique_keywords = set() 31 | if resp.status_code == 200: 32 | licenses = license_from_dict(resp.json()) 33 | for license in licenses: 34 | keywords=license.keywords 35 | #print(f"Debug: {keywords}") 36 | if len(keywords) > 0: 37 | for keyword in keywords: 38 | unique_keywords.add(str(keyword.value)) 39 | print("Please choose a keyword key from this list:",unique_keywords) 40 | else: 41 | print("Error fetching licenses while listing all keywords:", resp.text) 42 | else: 43 | print("Error {resp.text} while searching licenses for keyword: {args.keyword}") 44 | 45 | elif args.steward: 46 | # Search licenses by steward 47 | resp = requests.get(f"{base_url}?steward={args.steward}") 48 | if resp.status_code == 200: 49 | licenses = license_from_dict(resp.json()) 50 | if len(licenses)>0: 51 | print_licenses_table_with_steward(licenses) 52 | else: 53 | print("No licenses found for steward:", args.steward) 54 | # Get all licenses and then stewards for display 55 | resp = requests.get(base_url) 56 | unique_stewards = set() 57 | if resp.status_code == 200: 58 | licenses = license_from_dict(resp.json()) 59 | for license in licenses: 60 | if len(license.stewards) > 0: 61 | unique_stewards.update(license.stewards) 62 | print("Please choose a steward key from this list:",unique_stewards) 63 | else: 64 | print("Error fetching licenses while listing all stewards:", resp.text) 65 | else: 66 | print("Error {resp.text} while searching licenses for steward:{args.steward}") 67 | 68 | elif args.search: 69 | # Search licenses by name 70 | resp = requests.get(f"{base_url}?name={args.search}") 71 | if resp.status_code == 200: 72 | licenses = license_from_dict(resp.json()) 73 | print_licenses_table(licenses) 74 | else: 75 | print("Error searching licenses:", resp.text) 76 | elif args.spdx_id: 77 | # Try to fetch license by SPDX ID 78 | spdx_id = args.spdx_id 79 | resp = requests.get(f"{base_url}/{spdx_id}") 80 | if resp.status_code == 404: 81 | data = resp.json() 82 | if "error" in data and data["error"] == "License not found.": 83 | print(f"License '{spdx_id}' not found.") 84 | print("Searching for similar licenses...") 85 | resp2 = requests.get(f"{base_url}?name={spdx_id}") 86 | if resp2.status_code == 200: 87 | licenses = license_from_dict(resp2.json()) 88 | if licenses: 89 | print("Are you looking for one of these licenses?") 90 | print_licenses_table(licenses) 91 | else: 92 | print("No similar licenses found.") 93 | else: 94 | print("Error searching licenses:", resp2.text) 95 | else: 96 | print(data) 97 | elif resp.status_code == 200: 98 | data = resp.json() 99 | # Print details for the found license 100 | licenses = license_from_dict([data]) 101 | print_license_details_table(licenses[0]) 102 | else: 103 | print("Error fetching license:", resp.text) 104 | else: 105 | # Print all licenses 106 | resp = requests.get(base_url) 107 | if resp.status_code == 200: 108 | licenses = license_from_dict(resp.json()) 109 | print_licenses_table(licenses) 110 | else: 111 | print("Error fetching licenses:", resp.text) 112 | 113 | if __name__ == "__main__": 114 | main() -------------------------------------------------------------------------------- /src/osilic/model.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from dataclasses import dataclass 3 | from typing import Optional, Any, List, TypeVar, Type, cast, Callable 4 | from tabulate import tabulate 5 | import textwrap 6 | 7 | 8 | T = TypeVar("T") 9 | EnumT = TypeVar("EnumT", bound=Enum) 10 | 11 | 12 | def from_str(x: Any) -> str: 13 | assert isinstance(x, str) 14 | return x 15 | 16 | 17 | def from_none(x: Any) -> Any: 18 | assert x is None 19 | return x 20 | 21 | 22 | def from_union(fs, x): 23 | for f in fs: 24 | try: 25 | return f(x) 26 | except: 27 | pass 28 | assert False 29 | 30 | 31 | def to_class(c: Type[T], x: Any) -> dict: 32 | assert isinstance(x, c) 33 | return cast(Any, x).to_dict() 34 | 35 | 36 | def from_bool(x: Any) -> bool: 37 | assert isinstance(x, bool) 38 | return x 39 | 40 | 41 | def from_list(f: Callable[[Any], T], x: Any) -> List[T]: 42 | assert isinstance(x, list) 43 | return [f(y) for y in x] 44 | 45 | 46 | def to_enum(c: Type[EnumT], x: Any) -> EnumT: 47 | assert isinstance(x, c) 48 | return x.value 49 | 50 | 51 | class Keyword(Enum): 52 | INTERNATIONAL = "international" 53 | NON_REUSABLE = "non-reusable" 54 | OTHER_MISCELLANEOUS = "other-miscellaneous" 55 | POPULAR_STRONG_COMMUNITY = "popular-strong-community" 56 | REDUNDANT_WITH_MORE_POPULAR = "redundant-with-more-popular" 57 | SPECIAL_PURPOSE = "special-purpose" 58 | SUPERSEDED = "superseded" 59 | UNCATEGORIZED = "uncategorized" 60 | VOLUNTARILY_RETIRED = "voluntarily-retired" 61 | 62 | 63 | @dataclass 64 | class Collection: 65 | href: Optional[str] = None 66 | 67 | @staticmethod 68 | def from_dict(obj: Any) -> 'Collection': 69 | assert isinstance(obj, dict) 70 | href = from_union([from_str, from_none], obj.get("href")) 71 | return Collection(href) 72 | 73 | def to_dict(self) -> dict: 74 | result: dict = {} 75 | if self.href is not None: 76 | result["href"] = from_union([from_str, from_none], self.href) 77 | return result 78 | 79 | 80 | @dataclass 81 | class Links: 82 | links_self: Optional[Collection] = None 83 | html: Optional[Collection] = None 84 | collection: Optional[Collection] = None 85 | 86 | @staticmethod 87 | def from_dict(obj: Any) -> 'Links': 88 | assert isinstance(obj, dict) 89 | links_self = from_union([Collection.from_dict, from_none], obj.get("self")) 90 | html = from_union([Collection.from_dict, from_none], obj.get("html")) 91 | collection = from_union([Collection.from_dict, from_none], obj.get("collection")) 92 | return Links(links_self, html, collection) 93 | 94 | def to_dict(self) -> dict: 95 | result: dict = {} 96 | if self.links_self is not None: 97 | result["self"] = from_union([lambda x: to_class(Collection, x), from_none], self.links_self) 98 | if self.html is not None: 99 | result["html"] = from_union([lambda x: to_class(Collection, x), from_none], self.html) 100 | if self.collection is not None: 101 | result["collection"] = from_union([lambda x: to_class(Collection, x), from_none], self.collection) 102 | return result 103 | 104 | 105 | @dataclass 106 | class License: 107 | id: Optional[str] = None 108 | name: Optional[str] = None 109 | spdx_id: Optional[str] = None 110 | version: Optional[str] = None 111 | submission_date: Optional[str] = None 112 | submission_url: Optional[str] = None 113 | submitter_name: Optional[str] = None 114 | approved: Optional[bool] = None 115 | approval_date: Optional[str] = None 116 | license_steward_version: Optional[str] = None 117 | license_steward_url: Optional[str] = None 118 | board_minutes: Optional[str] = None 119 | stewards: Optional[List[str]] = None 120 | keywords: Optional[List[Keyword]] = None 121 | links: Optional[Links] = None 122 | 123 | @staticmethod 124 | def from_dict(obj: Any) -> 'License': 125 | assert isinstance(obj, dict) 126 | id = from_union([from_str, from_none], obj.get("id")) 127 | name = from_union([from_str, from_none], obj.get("name")) 128 | spdx_id = from_union([from_str, from_none], obj.get("spdx_id")) 129 | version = from_union([from_str, from_none], obj.get("version")) 130 | submission_date = from_union([from_str, from_none], obj.get("submission_date")) 131 | submission_url = from_union([from_str, from_none], obj.get("submission_url")) 132 | submitter_name = from_union([from_str, from_none], obj.get("submitter_name")) 133 | approved = from_union([from_bool, from_none], obj.get("approved")) 134 | approval_date = from_union([from_str, from_none], obj.get("approval_date")) 135 | license_steward_version = from_union([from_str, from_none], obj.get("license_steward_version")) 136 | license_steward_url = from_union([from_str, from_none], obj.get("license_steward_url")) 137 | board_minutes = from_union([from_str, from_none], obj.get("board_minutes")) 138 | stewards = from_union([lambda x: from_list(from_str, x), from_none], obj.get("stewards")) 139 | keywords = from_union([lambda x: from_list(Keyword, x), from_none], obj.get("keywords")) 140 | links = from_union([Links.from_dict, from_none], obj.get("_links")) 141 | return License(id, name, spdx_id, version, submission_date, submission_url, submitter_name, approved, approval_date, license_steward_version, license_steward_url, board_minutes, stewards, keywords, links) 142 | 143 | def to_dict(self) -> dict: 144 | result: dict = {} 145 | if self.id is not None: 146 | result["id"] = from_union([from_str, from_none], self.id) 147 | if self.name is not None: 148 | result["name"] = from_union([from_str, from_none], self.name) 149 | if self.spdx_id is not None: 150 | result["spdx_id"] = from_union([from_str, from_none], self.spdx_id) 151 | if self.version is not None: 152 | result["version"] = from_union([from_str, from_none], self.version) 153 | if self.submission_date is not None: 154 | result["submission_date"] = from_union([from_str, from_none], self.submission_date) 155 | if self.submission_url is not None: 156 | result["submission_url"] = from_union([from_str, from_none], self.submission_url) 157 | if self.submitter_name is not None: 158 | result["submitter_name"] = from_union([from_str, from_none], self.submitter_name) 159 | if self.approved is not None: 160 | result["approved"] = from_union([from_bool, from_none], self.approved) 161 | if self.approval_date is not None: 162 | result["approval_date"] = from_union([from_str, from_none], self.approval_date) 163 | if self.license_steward_version is not None: 164 | result["license_steward_version"] = from_union([from_str, from_none], self.license_steward_version) 165 | if self.license_steward_url is not None: 166 | result["license_steward_url"] = from_union([from_str, from_none], self.license_steward_url) 167 | if self.board_minutes is not None: 168 | result["board_minutes"] = from_union([from_str, from_none], self.board_minutes) 169 | if self.stewards is not None: 170 | result["stewards"] = from_union([lambda x: from_list(from_str, x), from_none], self.stewards) 171 | if self.keywords is not None: 172 | result["keywords"] = from_union([lambda x: from_list(lambda x: to_enum(Keyword, x), x), from_none], self.keywords) 173 | if self.links is not None: 174 | result["_links"] = from_union([lambda x: to_class(Links, x), from_none], self.links) 175 | return result 176 | 177 | 178 | def license_from_dict(s: Any) -> List[License]: 179 | return from_list(License.from_dict, s) 180 | 181 | 182 | def license_to_dict(x: List[License]) -> Any: 183 | return from_list(lambda x: to_class(License, x), x) 184 | 185 | 186 | def print_licenses_table(licenses: List[License], width: int = 45) -> None: 187 | """ 188 | Print a list of License objects in a nicely formatted table, wrapping long text fields except Links. 189 | Excludes Submission URL and Board Minutes columns. 190 | """ 191 | headers = [ 192 | "ID", "Name", "SPDX ID", "Approved", "Keywords", "Links" 193 | ] 194 | table = [] 195 | for lic in licenses: 196 | links_str = str(lic.links) if lic.links else "" 197 | links_str = "\n".join([part.strip() for part in links_str.split(",")]) if links_str else "" 198 | row = [ 199 | lic.id or "", 200 | lic.name or "", 201 | lic.spdx_id or "", 202 | str(lic.approved) if lic.approved is not None else "", 203 | ", ".join([k.value for k in lic.keywords]) if lic.keywords else "", 204 | links_str 205 | ] 206 | # Wrap each cell except Links 207 | row = ["\n".join(textwrap.wrap(str(cell), width)) if i != 5 and len(str(cell)) > width else str(cell) for i, cell in enumerate(row)] 208 | table.append(row) 209 | print(tabulate(table, headers=headers, tablefmt="grid")) 210 | 211 | def print_licenses_table_with_steward(licenses: List[License], width: int = 45) -> None: 212 | """ 213 | Print a list of License objects in a nicely formatted table, wrapping long text fields except Links. 214 | Excludes Submission URL and Board Minutes columns. 215 | """ 216 | headers = [ 217 | "ID", "Name", "SPDX ID", "Approved", "Stewards", "Links" 218 | ] 219 | table = [] 220 | for lic in licenses: 221 | links_str = str(lic.links) if lic.links else "" 222 | links_str = "\n".join([part.strip() for part in links_str.split(",")]) if links_str else "" 223 | row = [ 224 | lic.id or "", 225 | lic.name or "", 226 | lic.spdx_id or "", 227 | str(lic.approved) if lic.approved is not None else "", 228 | ", ".join([k for k in lic.stewards]) if lic.stewards else "", 229 | links_str 230 | ] 231 | # Wrap each cell except Links 232 | row = ["\n".join(textwrap.wrap(str(cell), width)) if i != 5 and len(str(cell)) > width else str(cell) for i, cell in enumerate(row)] 233 | table.append(row) 234 | print(tabulate(table, headers=headers, tablefmt="grid")) 235 | 236 | def print_license_details_table(license: License, width: int = 45) -> None: 237 | """ 238 | Print all fields of a single License object vertically in a table, wrapping long text fields except Links, Submission URL, and Board Minutes. 239 | """ 240 | links_str = str(license.links) if license.links else "" 241 | links_str = "\n".join([part.strip() for part in links_str.split(",")]) if links_str else "" 242 | fields = [ 243 | ("ID", license.id), 244 | ("Name", license.name), 245 | ("SPDX ID", license.spdx_id), 246 | ("Version", license.version), 247 | ("Submission Date", license.submission_date), 248 | ("Submission URL", license.submission_url), 249 | ("Submitter Name", license.submitter_name), 250 | ("Approved", str(license.approved) if license.approved is not None else ""), 251 | ("Approval Date", license.approval_date), 252 | ("License Steward Version", license.license_steward_version), 253 | ("License Steward URL", license.license_steward_url), 254 | ("Board Minutes", license.board_minutes), 255 | ("Stewards", ", ".join(license.stewards) if license.stewards else ""), 256 | ("Keywords", ", ".join([k.value for k in license.keywords]) if license.keywords else ""), 257 | ("Links", links_str) 258 | ] 259 | # Wrap values if needed, except Links, Submission URL, Board Minutes 260 | wrapped_fields = [ 261 | (name, value if name in ["Links", "Submission URL", "Board Minutes"] else ("\n".join(textwrap.wrap(str(value), width)) if value and len(str(value)) > width else str(value) if value else "")) 262 | for name, value in fields 263 | ] 264 | print(tabulate(wrapped_fields, headers=["Field", "Value"], tablefmt="grid")) 265 | --------------------------------------------------------------------------------