1+ """LanguageTool download module."""
2+
13import logging
24import os
35import re
79import tqdm
810from typing import IO , Dict , Optional , Tuple
911import zipfile
12+ from datetime import datetime
1013
1114from shutil import which
1215from urllib .parse import urljoin
2427
2528
2629# Get download host from environment or default.
27- BASE_URL = os .environ .get ('LTP_DOWNLOAD_HOST' , 'https://www.languagetool.org/download/' )
28- FILENAME = 'LanguageTool-{version}.zip'
30+ BASE_URL_SNAPSHOT = os .environ .get ('LTP_DOWNLOAD_HOST_SNAPSHOT' , 'https://internal1.languagetool.org/snapshots/' )
31+ FILENAME_SNAPSHOT = 'LanguageTool-{version}-snapshot.zip'
32+ BASE_URL_RELEASE = os .environ .get ('LTP_DOWNLOAD_HOST_RELEASE' , 'https://www.languagetool.org/download/' )
33+ FILENAME_RELEASE = 'LanguageTool-{version}.zip'
2934
30- LTP_DOWNLOAD_VERSION = '6.6'
35+ LTP_DOWNLOAD_VERSION = 'latest'
36+ LT_SNAPSHOT_CURRENT_VERSION = '6.7-SNAPSHOT'
3137
3238JAVA_VERSION_REGEX = re .compile (
3339 r'^(?:java|openjdk) version "(?P<major1>\d+)(|\.(?P<major2>\d+)\.[^"]+)"' ,
@@ -63,16 +69,16 @@ def parse_java_version(version_text: str) -> Tuple[int, int]:
6369 return (major1 , major2 )
6470
6571
66- def confirm_java_compatibility () -> bool :
72+ def confirm_java_compatibility (language_tool_version : Optional [ str ] = LTP_DOWNLOAD_VERSION ) -> None :
6773 """
6874 Confirms if the installed Java version is compatible with language-tool-python.
69- This function checks if Java is installed and verifies that the major version is at least 8.
75+ This function checks if Java is installed and verifies that the major version is at least 8 or 17 (depending on the LanguageTool version) .
7076 It raises an error if Java is not installed or if the version is incompatible.
7177
78+ :param language_tool_version: The version of LanguageTool to check compatibility for.
79+ :type language_tool_version: Optional[str]
7280 :raises ModuleNotFoundError: If no Java installation is detected.
73- :raises SystemError: If the detected Java version is less than 8.
74- :return: True if the Java version is compatible.
75- :rtype: bool
81+ :raises SystemError: If the detected Java version is less than the required version.
7682 """
7783
7884 java_path = which ('java' )
@@ -87,17 +93,24 @@ def confirm_java_compatibility() -> bool:
8793 universal_newlines = True )
8894
8995 major_version , minor_version = parse_java_version (output )
96+ version_date_cutoff = datetime .strptime ('2025-03-27' , '%Y-%m-%d' )
97+ is_old_version = (
98+ language_tool_version != 'latest' and (
99+ (re .match (r'^\d+\.\d+$' , language_tool_version ) and language_tool_version < '6.6' ) or
100+ (re .match (r'^\d{8}$' , language_tool_version ) and datetime .strptime (language_tool_version , '%Y%m%d' ) < version_date_cutoff )
101+ )
102+ )
103+
90104 # Some installs of java show the version number like `14.0.1`
91105 # and others show `1.14.0.1`
92- # (with a leading 1). We want to support both,
93- # as long as the major version is >= 8.
106+ # (with a leading 1). We want to support both.
94107 # (See softwareengineering.stackexchange.com/questions/175075/why-is-java-version-1-x-referred-to-as-java-x)
95- if major_version == 1 and minor_version >= 8 :
96- return True
97- elif major_version >= 8 :
98- return True
108+ if is_old_version :
109+ if (major_version == 1 and minor_version < 8 ) or (major_version != 1 and major_version < 8 ):
110+ raise SystemError (f'Detected java { major_version } .{ minor_version } . LanguageTool requires Java >= 8 for version { language_tool_version } .' )
99111 else :
100- raise SystemError (f'Detected java { major_version } .{ minor_version } . LanguageTool requires Java >= 8.' )
112+ if (major_version == 1 and minor_version < 17 ) or (major_version != 1 and major_version < 17 ):
113+ raise SystemError (f'Detected java { major_version } .{ minor_version } . LanguageTool requires Java >= 17 for version { language_tool_version } .' )
101114
102115
103116def get_common_prefix (z : zipfile .ZipFile ) -> Optional [str ]:
@@ -133,7 +146,7 @@ def http_get(url: str, out_file: IO[bytes], proxies: Optional[Dict[str, str]] =
133146 total = int (content_length ) if content_length is not None else None
134147 if req .status_code == 404 :
135148 raise PathError (f'Could not find at URL { url } . The given version may not exist or is no longer available.' )
136- version = re . search ( r'(\d+\.\d+)' , url ). group ( 1 )
149+ version = url . split ( '/' )[ - 1 ]. split ( '-' )[ 1 ]. replace ( '-snapshot' , '' ). replace ( '.zip' , '' )
137150 progress = tqdm .tqdm (unit = "B" , unit_scale = True , total = total ,
138151 desc = f'Downloading LanguageTool { version } ' )
139152 for chunk in req .iter_content (chunk_size = 1024 ):
@@ -143,18 +156,18 @@ def http_get(url: str, out_file: IO[bytes], proxies: Optional[Dict[str, str]] =
143156 progress .close ()
144157
145158
146- def unzip_file (temp_file : str , directory_to_extract_to : str ) -> None :
159+ def unzip_file (temp_file_name : str , directory_to_extract_to : str ) -> None :
147160 """
148161 Unzips a zip file to a specified directory.
149162
150- :param temp_file : A temporary file object representing the zip file to be extracted.
151- :type temp_file : str
163+ :param temp_file_name : A temporary file object representing the zip file to be extracted.
164+ :type temp_file_name : str
152165 :param directory_to_extract_to: The directory where the contents of the zip file will be extracted.
153166 :type directory_to_extract_to: str
154167 """
155-
156- logger .info (f'Unzipping { temp_file . name } to { directory_to_extract_to } .' )
157- with zipfile .ZipFile (temp_file . name , 'r' ) as zip_ref :
168+
169+ logger .info (f'Unzipping { temp_file_name } to { directory_to_extract_to } .' )
170+ with zipfile .ZipFile (temp_file_name , 'r' ) as zip_ref :
158171 zip_ref .extractall (directory_to_extract_to )
159172
160173
@@ -173,7 +186,7 @@ def download_zip(url: str, directory: str) -> None:
173186 # Close the file so we can extract it.
174187 downloaded_file .close ()
175188 # Extract zip file to path.
176- unzip_file (downloaded_file , directory )
189+ unzip_file (downloaded_file . name , directory )
177190 # Remove the temporary file.
178191 os .remove (downloaded_file .name )
179192 # Tell the user the download path.
@@ -192,9 +205,10 @@ def download_lt(language_tool_version: Optional[str] = LTP_DOWNLOAD_VERSION) ->
192205 LTP_DOWNLOAD_VERSION is used.
193206 :type language_tool_version: Optional[str]
194207 :raises AssertionError: If the download folder is not a directory.
208+ :raises ValueError: If the specified version format is invalid.
195209 """
196210
197- confirm_java_compatibility ()
211+ confirm_java_compatibility (language_tool_version )
198212
199213 download_folder = get_language_tool_download_path ()
200214
@@ -211,11 +225,23 @@ def download_lt(language_tool_version: Optional[str] = LTP_DOWNLOAD_VERSION) ->
211225
212226 if language_tool_version :
213227 version = language_tool_version
214- filename = FILENAME .format (version = version )
215- language_tool_download_url = urljoin (BASE_URL , filename )
228+ if re .match (r'^\d+\.\d+$' , version ):
229+ filename = FILENAME_RELEASE .format (version = version )
230+ language_tool_download_url = urljoin (BASE_URL_RELEASE , filename )
231+ elif version == "latest" :
232+ filename = FILENAME_SNAPSHOT .format (version = version )
233+ language_tool_download_url = urljoin (BASE_URL_SNAPSHOT , filename )
234+ else :
235+ raise ValueError (
236+ f"You can only download a specific version of LanguageTool if it is "
237+ f"formatted like 'x.y' (e.g. '5.4'). The version you provided is { version } ."
238+ f"You can also use 'latest' to download the latest snapshot of LanguageTool."
239+ )
216240 dirname , _ = os .path .splitext (filename )
241+ dirname = dirname .replace ('latest' , LT_SNAPSHOT_CURRENT_VERSION )
242+ if version == "latest" :
243+ dirname = f"LanguageTool-{ LT_SNAPSHOT_CURRENT_VERSION } "
217244 extract_path = os .path .join (download_folder , dirname )
218245
219- if extract_path in old_path_list :
220- return
221- download_zip (language_tool_download_url , download_folder )
246+ if extract_path not in old_path_list :
247+ download_zip (language_tool_download_url , download_folder )
0 commit comments