├── src ├── create_package_setup.py ├── check_inits.py ├── get_installed_packages.py └── main.py ├── LICENSE └── README.md /src/create_package_setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | -------------------------------------------------------------------------------- /src/check_inits.py: -------------------------------------------------------------------------------- 1 | import os 2 | import beautifish.icons as bfi 3 | import beautifish.colors as bfc 4 | 5 | def check_all_folders_got_init(root_path:str=os.getcwd()): 6 | # traverse through all subfolders under this root path given 7 | for folder_path, _, _ in os.walk(root_path): 8 | init_file = os.path.join(folder_path,"__init__.py") 9 | 10 | # create __init__.py if it doesnt exist 11 | if not os.path.exists(init_file): 12 | print(f"Creating __init__.py file at: {folder_path}") 13 | with open(init_file,"w") as f: 14 | pass # to create an empty __init__.py in this directory 15 | else: 16 | print(f"{bfc.cyan_text(bfi.DOT)} FOUND __init__.py file at {folder_path}") 17 | 18 | return 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kushal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/get_installed_packages.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import ast 4 | 5 | def extract_imports(file_path:str): 6 | if file_path=="": 7 | return 8 | 9 | with open(file_path,'r',encoding='utf-8') as file: 10 | tree = ast.parse(file.read(),file_path) 11 | 12 | user_imports = set() 13 | for node in ast.walk(tree): 14 | if isinstance(node, ast.Import): 15 | for alias in node.names: 16 | user_imports.add(alias.name) 17 | elif isinstance(node, ast.ImportFrom): 18 | user_imports.add(node.module) 19 | 20 | return user_imports 21 | 22 | 23 | 24 | def get_user_installed_packages(root_path:str=os.getcwd()): 25 | packages_used = set() 26 | system_native_packages = set(sys.builtin_module_names) 27 | 28 | for folder_path, _, files in os.walk(root_path): 29 | for file_name in files: 30 | if file_name.endswith('.py'): 31 | file_path = os.path.join(folder_path,file_name) 32 | user_imports = extract_imports(file_path=file_path) 33 | packages_used.update(user_imports) 34 | 35 | imported_packages = packages_used - system_native_packages 36 | 37 | return imported_packages -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pipme: Package bundler for PyPI 2 | 3 |   4 |   5 |   6 |   7 | 8 | pipme is a tool that simplifies the process of publishing Python projects to PyPI. It automates the process of packaging and uploading projects, making it easy for developers to share their Python packages with the world. 9 | 10 | ```bash 11 | pip install pipme 12 | ``` 13 | 14 |
15 | 16 | ## ✨ Features 17 | 18 | - **Easy publishing**: Publish Python projects to PyPI with a single command. 19 | - **Seamless integration**: Secure and reliable package uploads. 20 | - **Automatic packaging**: Automatically creates distribution packages from your project files. 21 | - **Simple usage**: Just provide your PyPI access token, and pipme takes care of the rest. 22 | 23 |
24 | 25 | ## 🌻 Usage 26 | 27 | To use pipme, simply install it via pip: 28 | 29 | ```bash 30 | pip install pipme 31 | ``` 32 | 33 | Then, navigate to your project root directory and run the following command to publish your project to PyPI: 34 | 35 | ```bash 36 | pipme 37 | ``` 38 | 39 | - creates new package folder with your project code inside it 40 | - adds `__init__.py` files to every directory as a necessary step 41 | - checks if such pip package already exists or not 42 | - creates `dist_wheel` to create the build folder 43 | - uploads to pip after requesting `access_token` from user 44 | 45 |
46 | 47 | ## 🤝 Contributions 48 | 49 | Contributions are welcome! 50 | 51 | If you have ideas for improvements, new features, or bug fixes, please feel free to open an issue or submit a pull request. 52 | 53 | ## ⚖️ License 54 | 55 | This project is licensed under the MIT License - see the LICENSE file for details. 56 | 57 |
58 | 59 |

 Kushal Kumar 2025 • All rights reserved

60 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shutil 4 | import requests 5 | import subprocess 6 | 7 | """ 8 | # valid package name = valid_user_package_name 9 | # valid package version = ask_user_version 10 | # parent_dir = parent_dir 11 | # packages installed = packages_installed 12 | # package folder is created with setup.py and markdown.md files in it 13 | """ 14 | 15 | 16 | # beautifish imports ++++++++++++++++++++++++++++++++++++++ 17 | import beautifish.templates as bft 18 | import beautifish.colors as bfc 19 | import beautifish.decorators as bfd 20 | # file imports ++++++++++++++++++++++++++++++++++++++ 21 | from pipme.check_inits import * 22 | from pipme.get_installed_packages import get_user_installed_packages 23 | # +++++++++++++++++++++++++++++++++++++++++++++++++++ 24 | 25 | command = { 26 | "bdist_wheel":"python setup.py sdist bdist_wheel", 27 | "twine_upload":"twine upload --repository-url https://upload.pypi.org/legacy/ -u __token__ -p dist/*" 28 | } 29 | 30 | # ———————————————————————————————————————————————————————————————————————————————————————— 31 | # UTIL FUNCTIONS ————————————————————————————————————————————————————————————————————————— 32 | 33 | def is_valid_version(input_string): 34 | pattern = r'^\d+(\.\d+){2}$' 35 | return re.match(pattern, input_string) is not None 36 | 37 | 38 | def convert_to_valid_package_name(name): 39 | # Replace non-ASCII letters, digits, underscores, and hyphens with underscores 40 | valid_name = re.sub(r'[^a-zA-Z0-9_-]', '_', name) 41 | # Ensure the name starts and ends with an ASCII letter or digit 42 | valid_name = re.sub(r'^[^a-zA-Z0-9]*|[^a-zA-Z0-9]*$', '', valid_name) 43 | # Convert the name to lowercase 44 | valid_name = valid_name.lower() 45 | return valid_name 46 | 47 | 48 | def create_package_and_setup(parent_dir,package_name:str): 49 | SETUPPY_FILE_CODE = """\ 50 | from setuptools import setup, find_packages 51 | 52 | setup( 53 | name='', 54 | version='', 55 | packages=find_packages(), 56 | install_requires=[] 57 | ) 58 | """ 59 | package_folder = os.path.join(parent_dir, package_name) 60 | os.makedirs(package_folder,exist_ok=True) 61 | 62 | # create setup.py file in new folder 63 | setupPy_file_path = os.path.join(package_folder,"setup.py") 64 | with open(setupPy_file_path,"w") as f: 65 | f.write(SETUPPY_FILE_CODE) 66 | 67 | 68 | # create readme.md 69 | readmeMD_file_path = os.path.join(package_folder,"readme.md") 70 | with open(readmeMD_file_path,"w") as f: 71 | pass 72 | 73 | 74 | 75 | def check_package_exists_in_pypi(package_name:str): 76 | PYPI_API_ENDPOINT = f"https://pypi.org/pypi/{package_name}/json" 77 | response = requests.get(PYPI_API_ENDPOINT) 78 | if response.status_code==200: 79 | return False 80 | elif response.status_code==404: 81 | return True 82 | else: 83 | print("Unexpected status code while reaching out to PyPI API:",response.status_code) 84 | return None 85 | 86 | 87 | def migrate_files(source_path,destination_path): 88 | try: 89 | shutil.copytree(source_path, destination_path) 90 | print(f"Files migrated to new folder...") 91 | except FileExistsError: 92 | print(f"Destination directory {destination_path} already exists") 93 | 94 | 95 | def modify_setup_file(setup_file_path,package_name:str,version:str,packages_to_install:list): 96 | # reading setup.py contents 97 | with open(setup_file_path,'r') as file: 98 | setup_contents = file.readlines() 99 | 100 | # updating name and version attributes 101 | modifications = [] 102 | for line in setup_contents: 103 | if line.strip().startswith('name='): 104 | modifications.append(f" name='{package_name}',\n") 105 | elif line.strip().startswith('version='): 106 | modifications.append(f" version='{version}',\n") 107 | elif line.strip().startswith('install_requires='): 108 | arr = "" 109 | for package in packages_to_install: 110 | arr += f"'{package}'," 111 | arr = arr[:len(arr)-1] 112 | modifications.append(f" install_requires=[{arr}],\n") 113 | else: 114 | modifications.append(line) 115 | 116 | # write into the setup.py with modifications 117 | with open(setup_file_path,'w') as file: 118 | file.writelines(modifications) 119 | 120 | 121 | def change_relative_imports(root_folder, old_package_name, new_package_name): 122 | for folder_path, _, files in os.walk(root_folder): 123 | for file_name in files: 124 | if file_name.endswith('.py'): 125 | file_path = os.path.join(folder_path, file_name) 126 | with open(file_path, 'r') as f: 127 | content = f.read() 128 | # Use regular expression to find and replace relative imports 129 | new_content = re.sub( 130 | fr'from\s+{old_package_name}(\s+|\.)', 131 | fr'from {new_package_name}\g<1>', 132 | content 133 | ) 134 | # Write the modified content back to the file 135 | with open(file_path, 'w') as f: 136 | f.write(new_content) 137 | 138 | 139 | def rename_folder(old_folder_path,new_folder_name): 140 | # Get the parent directory of the folder 141 | parent_directory = os.path.dirname(old_folder_path) 142 | # Create the new folder path 143 | new_folder_path = os.path.join(parent_directory, new_folder_name) 144 | # Rename the folder 145 | os.rename(old_folder_path, new_folder_path) 146 | print(f"Folder '{old_folder_path}' renamed to '{new_folder_name}'") 147 | 148 | # ————————————————————————————————————————————————————————————————————————————————————————— 149 | # ————————————————————————————————————————————————————————————————————————————————————————— 150 | 151 | 152 | # ——— MAIN FUNCTION ——————————————————————————————————————————————————————————————————————— 153 | # ————————————————————————————————————————————————————————————————————————————————————————— 154 | def run_cli(shell_path:str=os.getcwd()): 155 | """ 156 | this function runs when user runs "pipme" in the shell 157 | """ 158 | 159 | bft.banner(msg="pipme",color="blue") 160 | 161 | # ask confirmation if this is the root folder where they want to pip ------------------------------------------ 162 | print(f"Using {bfc.blue_text(shell_path)} as root directory.") 163 | ask_user = bft.input(f"Continue? (y/N): ",color="blue") 164 | if ask_user=="" or ask_user.lower().split()=='n': 165 | return 166 | 167 | 168 | # ask if they registered at pypi.org and have an access token ready or not ------------------------------------ 169 | ask_user = input(f"Are you registered at {bfc.blue_text("https://pypi.org/")} and have an {bfc.blue_text("access token")} (y/N): ") 170 | if ask_user=='' or ask_user.lower().split()=='n': 171 | return 172 | 173 | 174 | # create __init__.py files to each subfolder ------------------------------------------------------------------ 175 | # this creates __init_.py files inside all subfolders if it doesnt already exist 176 | bft.seperator2(header="Scanning for __init__.py files") 177 | check_all_folders_got_init(shell_path) 178 | 179 | 180 | # get all packages imported into the folder ------------------------------------------------------------------- 181 | dummy = set() 182 | packages_installed = get_user_installed_packages(shell_path) 183 | for s in packages_installed: 184 | dummy.add(s.split('.')[0] if '.' in s else s) 185 | packages_installed = list(dummy) 186 | bft.success(msg=" ",title="Found packages used in project") 187 | bft.list(packages_installed) 188 | # this will go into the requirements array-------->> 189 | 190 | 191 | # get parent dir path ----------------------------------------------------------------------------------------- 192 | parent_dir = os.path.dirname(shell_path) 193 | user_package_name = os.path.basename(shell_path) 194 | valid_user_package_name = convert_to_valid_package_name(user_package_name) 195 | 196 | # check if this name already exists and owned by someone in pypi ---------------------------------------------- 197 | check_pip_existence = check_package_exists_in_pypi(valid_user_package_name) 198 | if(check_pip_existence==False or check_pip_existence==None): 199 | flag = True 200 | while flag: 201 | is_owner = input(f"Your package name '{valid_user_package_name}' already is owned by someone. If its owned by you, type 'yes': ") 202 | if(is_owner.lower()=="yes"): 203 | flag = False 204 | break 205 | new_input = input("Enter a different package name you wish to use: ") 206 | new_input = convert_to_valid_package_name(new_input) 207 | print(f"Reformatted to: {new_input}...") 208 | valid_user_package_name = new_input 209 | if(check_package_exists_in_pypi(new_input)==True): 210 | flag=False 211 | valid_user_package_name = new_input 212 | 213 | print(f"{bfi.DOT} Package name used: {bfc.cyan_text(valid_user_package_name)}") 214 | 215 | 216 | 217 | # ask user for the version they wish to make it ---------------------------------------------------------------- 218 | ask_user_version = input(f"What's the version you wish to give {bfc.gray_text("(default 1.0.0)")}: ") 219 | # check user input to version's authneticity 220 | if ask_user_version=="": 221 | ask_user_version = "1.0.0" 222 | while not is_valid_version(ask_user_version): 223 | bft.error("Unauthentic version syntax, must be of type num.num.num",mode="spaced") 224 | ask_user_version = input(f"What's the version you wish to give {bfc.gray_text("(default 1.0.0)")}: ") 225 | if ask_user_version=="": 226 | ask_user_version = "1.0.0" 227 | 228 | 229 | # create package folder and setup.py in it ---------------------------------------------------------------------- 230 | print(f"Creating package in parent dir and adding setup.py file to it") 231 | create_package_and_setup(parent_dir=parent_dir,package_name=f"{valid_user_package_name}_package") 232 | 233 | 234 | # copy shell_path to new folder --------------------------------------------------------------------------------- 235 | package_path = os.path.join(parent_dir, f"{valid_user_package_name}_package") 236 | 237 | 238 | # migrate files to package_path --------------------------------------------------------------------------------- 239 | print(f"Migrating files from {bfc.gray_text(shell_path)} to {bfc.orange_text(package_path)}") 240 | migrate_files(shell_path,os.path.join(package_path,valid_user_package_name)) 241 | 242 | 243 | # modify setup files to contain name and version number --------------------------------------------------------- 244 | print(f"Adding name and version to setup.py...") 245 | modify_setup_file(setup_file_path=os.path.join(package_path,"setup.py"),package_name=valid_user_package_name,version=ask_user_version,packages_to_install=packages_installed) 246 | 247 | 248 | # change relative imports --------------------------------------------------------------------------------------- 249 | if os.path.basename(shell_path)!=valid_user_package_name: 250 | print(f"Changing relative imports from {os.path.basename(shell_path)} to {valid_user_package_name}") 251 | change_relative_imports(os.path.join(package_path,valid_user_package_name),os.path.basename(shell_path),valid_user_package_name) 252 | 253 | 254 | # cross-check __init__.py files exist in new cleaned folder directory or not ------------------------------------ 255 | check_all_folders_got_init(os.path.join(package_path,valid_user_package_name)) 256 | 257 | 258 | # migrate to package directory in shell ------------------------------------------------------------------------- 259 | os.chdir(package_path) 260 | print("Migrated shell path to:",bfc.orange_text(os.getcwd())) 261 | 262 | 263 | # run command to create distwheel ------------------------------------------------------------------------------- 264 | """ the idea is when package will be installed, twine would also be installed already 265 | as such, its safe to assume twine isnt required as another installation dependency """ 266 | print(bfc.gray_text("Running command to create distwheel...")) 267 | distwheel_status = subprocess.run(command["bdist_wheel"],shell=True,capture_output=True,text=True) 268 | print(distwheel_status.stdout) 269 | 270 | 271 | # run command to upload using twine ----------------------------------------------------------------------------- 272 | # get access token 273 | access_token = bft.input("Enter the access token: ",color="blue") 274 | if len(access_token)<150: 275 | bft.error("The access token entered seems too short to be precise") 276 | return 277 | # run pip upload command 278 | print(bfc.cyan_text("Running command to upload to pip using twine and access token provided...")) 279 | upload_command = command["twine_upload"].replace("",access_token) 280 | twine_upload_status = subprocess.run(upload_command,shell=True,capture_output=True,text=True) 281 | print(twine_upload_status.stdout) 282 | 283 | print(bfc.orange_text("[^^] Thanks for using Pipme\n\n - by Kushal Kumar (am.kushal02@gmail.com)\n - https://kushalkumarsaha.com\n\n")) 284 | 285 | 286 | 287 | 288 | if __name__=="__main__": 289 | current_shell_path = os.getcwd() 290 | run_cli(current_shell_path) 291 | --------------------------------------------------------------------------------