11# ruff: noqa: TRY301, FBT002, UP007
22from __future__ import annotations
33
4- import os
4+ import importlib .resources
5+ import json
56from pathlib import Path
67from typing import Annotated , Optional , cast
78
8- import httpx
99import inquirer # type: ignore[import-untyped]
1010import typer
1111from cookiecutter .main import cookiecutter # type: ignore[import-untyped]
1212from inquirer .render .console import ConsoleRender # type: ignore[import-untyped]
1313from rich .progress import Progress , SpinnerColumn , TextColumn
1414
15- TEMPLATE_LIST_URL = 'https://api.github.com/repos/apify/crawlee-python/contents/templates'
16-
1715cli = typer .Typer (no_args_is_help = True )
1816
17+ template_directory = importlib .resources .files ('crawlee' ) / 'project_template'
18+ cookiecutter_json = json .load ((template_directory / 'cookiecutter.json' ).open ())
19+
20+ crawler_choices = cookiecutter_json ['crawler_type' ]
21+ http_client_choices = cookiecutter_json ['http_client' ]
22+ package_manager_choices = cookiecutter_json ['package_manager' ]
23+ default_start_url = cookiecutter_json ['start_url' ]
24+
1925
2026@cli .callback (invoke_without_command = True )
2127def callback (
@@ -64,25 +70,42 @@ def _prompt_for_project_name(initial_project_name: str | None) -> str:
6470 return project_name
6571
6672
67- def _prompt_for_template () -> str :
68- """Prompt the user to select a template from a list."""
69- # Fetch available templates
70- response = httpx .get (
71- TEMPLATE_LIST_URL ,
72- timeout = httpx .Timeout (10 ),
73- headers = [('Authorization' , f'Bearer { os .environ ["GH_TOKEN" ]} ' )] if 'GH_TOKEN' in os .environ else [],
73+ def _prompt_text (message : str , default : str ) -> str :
74+ return cast (
75+ str ,
76+ ConsoleRender ().render (
77+ inquirer .Text (
78+ name = 'text' ,
79+ message = message ,
80+ default = default ,
81+ validate = lambda _ , value : bool (value .strip ()),
82+ ),
83+ ),
7484 )
75- response .raise_for_status ()
76- template_choices = [item ['name' ] for item in response .json () if item ['type' ] == 'dir' ]
7785
78- # Prompt for template choice
86+
87+ def _prompt_choice (message : str , choices : list [str ]) -> str :
88+ """Prompt the user to pick one from a list of choices."""
7989 return cast (
8090 str ,
8191 ConsoleRender ().render (
8292 inquirer .List (
83- name = 'template' ,
84- message = 'Please select the template for your new Crawlee project' ,
85- choices = [(choice [0 ].upper () + choice [1 :], choice ) for choice in template_choices ],
93+ name = 'choice' ,
94+ message = message ,
95+ choices = [(choice [0 ].upper () + choice [1 :], choice ) for choice in choices ],
96+ ),
97+ ),
98+ )
99+
100+
101+ def _prompt_bool (message : str , * , default : bool ) -> bool :
102+ return cast (
103+ bool ,
104+ ConsoleRender ().render (
105+ inquirer .Confirm (
106+ name = 'confirm' ,
107+ message = message ,
108+ default = default ,
86109 ),
87110 ),
88111 )
@@ -92,26 +115,77 @@ def _prompt_for_template() -> str:
92115def create (
93116 project_name : Optional [str ] = typer .Argument (
94117 default = None ,
118+ show_default = False ,
95119 help = 'The name of the project and the directory that will be created to contain it. '
96120 'If none is given, you will be prompted.' ,
121+ ),
122+ crawler_type : Optional [str ] = typer .Option (
123+ None ,
124+ '--crawler-type' ,
125+ '--template' ,
126+ show_default = False ,
127+ help = 'The library that will be used for crawling in your crawler. If none is given, you will be prompted.' ,
128+ ),
129+ http_client : Optional [str ] = typer .Option (
130+ None ,
131+ show_default = False ,
132+ help = 'The library that will be used to make HTTP requests in your crawler. '
133+ 'If none is given, you will be prompted.' ,
134+ ),
135+ package_manager : Optional [str ] = typer .Option (
136+ default = None ,
97137 show_default = False ,
138+ help = 'Package manager to be used in the new project. If none is given, you will be prompted.' ,
98139 ),
99- template : Optional [str ] = typer .Option (
140+ start_url : Optional [str ] = typer .Option (
100141 default = None ,
101- help = 'The template to be used to create the project. If none is given, you will be prompted.' ,
102142 show_default = False ,
143+ help = 'The URL where crawling should start. If none is given, you will be prompted.' ,
144+ ),
145+ enable_apify_integration : Optional [bool ] = typer .Option (
146+ None ,
147+ '--apify/--no-apify' ,
148+ show_default = False ,
149+ help = 'Should Apify integration be set up for you? If not given, you will be prompted.' ,
103150 ),
104151) -> None :
105152 """Bootstrap a new Crawlee project."""
106153 try :
107154 # Prompt for project name if not provided.
108155 project_name = _prompt_for_project_name (project_name )
109156
110- # Prompt for template choice if not provided.
111- if template is None :
112- template = _prompt_for_template ()
157+ # Prompt for crawler_type if not provided.
158+ if crawler_type is None :
159+ crawler_type = _prompt_choice ('Please select the Crawler type' , crawler_choices )
160+
161+ # Prompt for http_client if not provided.
162+ if http_client is None :
163+ http_client = _prompt_choice ('Please select the HTTP client' , http_client_choices )
164+
165+ # Prompt for package manager if not provided.
166+ if package_manager is None :
167+ package_manager = _prompt_choice ('Please select the package manager' , package_manager_choices )
168+
169+ # Prompt for start URL
170+ if start_url is None :
171+ start_url = _prompt_text ('Please specify the start URL' , default = default_start_url )
172+
173+ # Ask about Apify integration if not explicitly configured
174+ if enable_apify_integration is None :
175+ enable_apify_integration = _prompt_bool ('Should Apify integration be set up for you?' , default = False )
176+
177+ if all (
178+ [
179+ project_name ,
180+ crawler_type ,
181+ http_client ,
182+ package_manager ,
183+ start_url ,
184+ enable_apify_integration is not None ,
185+ ]
186+ ):
187+ package_name = project_name .replace ('-' , '_' )
113188
114- if project_name and template :
115189 # Start the bootstrap process.
116190 with Progress (
117191 SpinnerColumn (),
@@ -120,21 +194,39 @@ def create(
120194 ) as progress :
121195 progress .add_task (description = 'Bootstrapping...' , total = None )
122196 cookiecutter (
123- template = 'gh:apify/crawlee-python' ,
124- directory = f'templates/{ template } ' ,
197+ template = str (template_directory ),
125198 no_input = True ,
126- extra_context = {'project_name' : project_name },
199+ extra_context = {
200+ 'project_name' : project_name ,
201+ 'package_manager' : package_manager ,
202+ 'crawler_type' : crawler_type ,
203+ 'http_client' : http_client ,
204+ 'enable_apify_integration' : enable_apify_integration ,
205+ 'start_url' : start_url ,
206+ },
127207 )
128208
129209 typer .echo (f'Your project "{ project_name } " was created.' )
130- typer .echo (
131- f'To run it, navigate to the directory: "cd { project_name } ", '
132- 'install dependencies with "poetry install", '
133- f'and run it using "poetry run python -m { project_name } ".'
134- )
210+
211+ if package_manager == 'manual' :
212+ typer .echo (
213+ f'To run it, navigate to the directory: "cd { project_name } ", '
214+ f'install the dependencies listed in "requirements.txt" '
215+ f'and run it using "python -m { package_name } ".'
216+ )
217+ elif package_manager == 'pip' :
218+ typer .echo (
219+ f'To run it, navigate to the directory: "cd { project_name } ", '
220+ f'activate the virtual environment in ".venv" ("source .venv/bin/activate") '
221+ f'and run your project using "python -m { package_name } ".'
222+ )
223+ elif package_manager == 'poetry' :
224+ typer .echo (
225+ f'To run it, navigate to the directory: "cd { project_name } ", '
226+ f'and run it using "poetry run python -m { package_name } ".'
227+ )
228+
135229 typer .echo (f'See the "{ project_name } /README.md" for more information.' )
136230
137- except httpx .HTTPStatusError as exc :
138- typer .echo (f'Failed to fetch templates: { exc } .' , err = True )
139231 except KeyboardInterrupt :
140232 typer .echo ('Operation cancelled by user.' )
0 commit comments