ccfx 0.9.0__tar.gz → 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ccfx
3
- Version: 0.9.0
3
+ Version: 1.0.0
4
4
  Summary: This package simplifies regular common actions for quick prototyping in a user friendly way
5
5
  Author-email: Celray James CHAWANDA <celray@chawanda.com>
6
6
  License-Expression: MIT
@@ -14,7 +14,7 @@ import os, sys
14
14
  import glob
15
15
  import warnings
16
16
  from netCDF4 import Dataset
17
- from osgeo import gdal, osr
17
+ from osgeo import gdal, ogr, osr
18
18
  import numpy
19
19
  from genericpath import exists
20
20
  import shutil
@@ -23,7 +23,6 @@ import pickle
23
23
  import time
24
24
  from shapely.geometry import box, Point
25
25
  import geopandas, pandas
26
- from osgeo import gdal, ogr, osr
27
26
  import py7zr
28
27
  import subprocess
29
28
  import multiprocessing
@@ -133,7 +132,7 @@ def guessMimeType(imagePath):
133
132
  return 'image/png'
134
133
 
135
134
 
136
- def downloadYoutubeVideo(url: str, dstDir: str, audioOnly: bool = False, dstFileName: Optional[str] = None ) -> str:
135
+ def downloadYoutubeVideo(url: str, dstDir: str, audioOnly: bool = False, cookiesFile: str = None, dstFileName: Optional[str] = None ) -> str:
137
136
  """
138
137
  Download from YouTube via yt-dlp.
139
138
 
@@ -154,6 +153,9 @@ def downloadYoutubeVideo(url: str, dstDir: str, audioOnly: bool = False, dstFile
154
153
 
155
154
  opts = {"outtmpl": template}
156
155
 
156
+ if cookiesFile:
157
+ opts["cookiefile"] = cookiesFile
158
+
157
159
  if audioOnly:
158
160
  opts.update({
159
161
  "format": "bestaudio/best",
@@ -163,6 +165,7 @@ def downloadYoutubeVideo(url: str, dstDir: str, audioOnly: bool = False, dstFile
163
165
  "preferredquality": "192",
164
166
  }],
165
167
  })
168
+
166
169
  else:
167
170
  # prefer a single MP4 file (progressive), fallback to any best if none
168
171
  opts["format"] = "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best"
@@ -181,6 +184,58 @@ def downloadYoutubeVideo(url: str, dstDir: str, audioOnly: bool = False, dstFile
181
184
  return os.path.join(dstDir, final)
182
185
 
183
186
 
187
+ def parseYoutubePlaylist(playlistUrl: str) -> list[str]:
188
+ """
189
+ Return a list of full video URLs contained in a YouTube playlist.
190
+
191
+ Args:
192
+ playlistUrl: Full URL of the playlist (the one with &list=… or /playlist?list=…).
193
+
194
+ Returns:
195
+ List of video URLs in the order reported by YouTube.
196
+ """
197
+ opts = {
198
+ "quiet": True,
199
+ "extract_flat": "in_playlist", # don’t recurse into each video
200
+ }
201
+
202
+ with yt_dlp.YoutubeDL(opts) as ytdl:
203
+ info = ytdl.extract_info(playlistUrl, download=False)
204
+
205
+ entries = info.get("entries", [])
206
+ return [f"https://www.youtube.com/watch?v={e['id']}" for e in entries if e.get("id")]
207
+
208
+
209
+ def parseYoutubeChannelVideos(channelUrl: str, maxItems: Optional[int] = None) -> list[str]:
210
+ """
211
+ Return a list of video URLs published on a channel.
212
+
213
+ Args:
214
+ channelUrl: Any canonical channel URL, e.g.
215
+ - https://www.youtube.com/@LinusTechTips
216
+ - https://www.youtube.com/channel/UCXuqSBlHAE6Xw-yeJA0Tunw
217
+ - https://www.youtube.com/c/NASA/videos
218
+ maxItems: Optional hard limit. If None, returns every video the API exposes.
219
+
220
+ Returns:
221
+ List of video URLs, newest-first (YouTube’s default order).
222
+ """
223
+ opts = {
224
+ "quiet": True,
225
+ "extract_flat": True, # treat the channel as one big “playlist”
226
+ "skip_download": True,
227
+ }
228
+
229
+ with yt_dlp.YoutubeDL(opts) as ytdl:
230
+ info = ytdl.extract_info(channelUrl, download=False)
231
+
232
+ entries = info.get("entries", [])
233
+ if maxItems is not None:
234
+ entries = entries[:maxItems]
235
+
236
+ return [f"https://www.youtube.com/watch?v={e['id']}" for e in entries if e.get("id")]
237
+
238
+
184
239
  def setMp3Metadata(fn, metadata, imagePath=None):
185
240
  '''
186
241
  This function takes a path to an mp3 and a metadata dictionary,
@@ -276,6 +331,39 @@ def deleteFile(filePath:str, v:bool = False) -> bool:
276
331
 
277
332
  return deleted
278
333
 
334
+
335
+ def alert(message:str, server:str = "http://ntfy.sh", topic:str = "pythonAlerts", attachment:str = None, messageTitle:str = "info", priority:int = None, tags:list = [], printIt:bool = True, v:bool = False) -> bool:
336
+ '''
337
+ This sends an alert to a given server in case you want to be notified of something
338
+ message : the message to send
339
+ server : the server to send the message to (default is http://ntfy.sh)
340
+ topic : the topic to send the message to (default is pythonAlerts)
341
+ attachment : a file to attach to the message (optional)
342
+ messageTitle : the title of the message (optional, default is info)
343
+ priority : the priority of the message (optional, default is None)
344
+ tags : a list of tags to add to the message (optional, default is empty list)
345
+ printIt : whether to print the message to the console (default is True)
346
+ v : verbose (default is False, set to True to print debug info)
347
+
348
+ return: True if the alert was sent successfully, False otherwise
349
+ '''
350
+ print(message) if printIt else None; header_data = {}
351
+ if not messageTitle is None: header_data["Title"] = messageTitle
352
+ if not priority is None: header_data["Priority"] = priority
353
+ if not len(tags) == 0: header_data["Tags"] = ",".join(tags)
354
+
355
+ try:
356
+ if v: print(f"sending alert to {server}/{topic}")
357
+ if not attachment is None:
358
+ header_data["Filename"] = getFileBaseName(attachment)
359
+ requests.put( f"{server}/{topic}", data=open(attachment, 'rb'), headers=header_data )
360
+ return True
361
+ try: requests.post(f"{server}/{topic}",data=message, headers=header_data )
362
+ except: return False
363
+ except: return False
364
+ return True
365
+
366
+
279
367
  def deletePath(path:str, v:bool = False) -> bool:
280
368
  '''
281
369
  Delete a directory
@@ -309,6 +397,38 @@ def downloadChunk(url, start, end, path):
309
397
  if chunk:
310
398
  f.write(chunk)
311
399
 
400
+
401
+ def formatStringBlock(input_str, max_chars=70):
402
+ '''
403
+ This function takes a string and formats it into a block of text
404
+ with a maximum number of characters per line.
405
+
406
+ input_str: the string to format
407
+ max_chars: the maximum number of characters per line (default is 70)
408
+
409
+ '''
410
+ words = input_str.split(' ')
411
+ lines = []
412
+ current_line = ""
413
+
414
+ for word in words:
415
+ # If adding the next word to the current line would exceed the max_chars limit
416
+ if len(current_line) + len(word) > max_chars:
417
+ # Append current line to lines and start a new one
418
+ lines.append(current_line.strip())
419
+ current_line = word
420
+ else:
421
+ # Add the word to the current line
422
+ current_line += " " + word
423
+
424
+ # Append any remaining words
425
+ lines.append(current_line.strip())
426
+
427
+ return '\n'.join(lines)
428
+
429
+
430
+
431
+
312
432
  def downloadFile(url, save_path, exists_action='resume', num_connections=5, v=False):
313
433
  if v:
314
434
  print(f"\ndownloading {url}")
@@ -643,21 +763,45 @@ def renameNetCDFvariable(input_file: str, output_file: str, old_var_name: str, n
643
763
  except subprocess.CalledProcessError as e:
644
764
  print(f"Error: {e.stderr}")
645
765
 
646
- def compressTo7z(input_dir: str, output_file: str):
766
+
767
+ def compressTo7z(input_dir: str, output_file: str, compressionLevel: int = 4, excludeExt: list = None, v: bool = False) -> None:
647
768
  """
648
769
  Compresses the contents of a directory to a .7z archive with maximum compression.
649
770
 
650
771
  :param input_dir: Path to the directory to compress
651
772
  :param output_file: Output .7z file path
773
+ :param compressionLevel: Compression level (0-9), default is 4 (maximum compression)
774
+ :param excludeExt: List of file extensions to exclude from compression
652
775
  """
776
+ if excludeExt is None:
777
+ excludeExt = []
778
+
653
779
  # Create the .7z archive with LZMA2 compression
654
- with py7zr.SevenZipFile(output_file, 'w', filters=[{'id': py7zr.FILTER_LZMA2, 'preset': 9}]) as archive:
780
+ with py7zr.SevenZipFile(output_file, 'w', filters=[{'id': py7zr.FILTER_LZMA2, 'preset': compressionLevel}]) as archive:
655
781
  # Add each item in the input directory, avoiding the top-level folder in the archive
656
782
  for root, _, files in os.walk(input_dir):
657
783
  for file in files:
658
784
  file_path = os.path.join(root, file)
785
+
786
+ # Skip excluded file extensions
787
+ if any(file.endswith(ext) for ext in excludeExt):
788
+ continue
659
789
  # Add file to the archive with a relative path to avoid including the 'tmp' folder itself
660
790
  archive.write(file_path, arcname=os.path.relpath(file_path, start=input_dir))
791
+ if v:
792
+ print(f"compressed {input_dir} to {output_file} with compression level {compressionLevel}.")
793
+
794
+
795
+ def uncompress(inputFile: str, outputDir: str, v: bool = False) -> None:
796
+ """
797
+ Extracts an archive supported by py7zr (.7z, .zip, .tar, .tar.gz, .tar.bz2, .xz, .tar.xz) to outputDir.
798
+ inputFile: Path to the input archive file
799
+ outputDir: Directory where the contents will be extracted
800
+ v: Verbose flag to print extraction status (default is False)
801
+ """
802
+ if not exists(outputDir): createPath(outputDir)
803
+ with py7zr.SevenZipFile(inputFile, 'r') as archive: archive.extractall(path=outputDir)
804
+ if v: print(f"extracted {inputFile} to {outputDir}.")
661
805
 
662
806
 
663
807
  def moveDirectory(srcDir:str, destDir:str, v:bool = False) -> bool:
@@ -744,6 +888,21 @@ def clipRasterByExtent(inFile: str, outFile: str, bounds: tuple) -> str:
744
888
  ds = None
745
889
  return outFile
746
890
 
891
+
892
+ def clipRasterByVector(inFile: str, outFile: str, vectorFile: str) -> str:
893
+ '''
894
+ Clips a raster using GDAL warp with a vector file
895
+ inFile: input raster path
896
+ outFile: output path
897
+ vectorFile: vector file path (e.g., shapefile or GeoJSON)
898
+ return: output path
899
+ '''
900
+ ds = gdal.Open(inFile)
901
+ gdal.Warp(outFile, ds, cutlineDSName=vectorFile, cropToCutline=True)
902
+ ds = None
903
+ return outFile
904
+
905
+
747
906
  def clipVectorByExtent(inFile: str, outFile: str, bounds: tuple) -> str:
748
907
  '''
749
908
  Clips a vector using GeoPandas
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ccfx
3
- Version: 0.9.0
3
+ Version: 1.0.0
4
4
  Summary: This package simplifies regular common actions for quick prototyping in a user friendly way
5
5
  Author-email: Celray James CHAWANDA <celray@chawanda.com>
6
6
  License-Expression: MIT
File without changes
File without changes
File without changes
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ccfx"
7
- version = "0.9.0"
7
+ version = "1.0.0"
8
8
  description = "This package simplifies regular common actions for quick prototyping in a user friendly way"
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes