ccfx 1.0.7__tar.gz → 1.0.9__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: 1.0.7
3
+ Version: 1.0.9
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
@@ -35,7 +35,7 @@ import math
35
35
  import requests
36
36
  from tqdm import tqdm
37
37
  import yt_dlp
38
- from typing import Optional
38
+ from typing import Optional, Any
39
39
  from datetime import datetime, timedelta
40
40
  from PIL import Image
41
41
 
@@ -72,7 +72,7 @@ def getExtension(filePath:str) -> str:
72
72
  return os.path.splitext(filePath)[1].lstrip('.')
73
73
 
74
74
 
75
- def getMp3Metadata(fn, imagePath=None):
75
+ def getMp3Metadata(fn: str, imagePath: Optional[str] = None) -> dict:
76
76
  '''
77
77
  This function takes a path to mp3 and returns a dictionary with
78
78
  the following keys:
@@ -127,7 +127,7 @@ def getMp3Metadata(fn, imagePath=None):
127
127
  return metadata
128
128
 
129
129
 
130
- def guessMimeType(imagePath):
130
+ def guessMimeType(imagePath: str) -> str:
131
131
  ext = os.path.splitext(imagePath.lower())[1]
132
132
  if ext in ['.jpg', '.jpeg']:
133
133
  return 'image/jpeg'
@@ -240,7 +240,7 @@ def parseYoutubeChannelVideos(channelUrl: str, maxItems: Optional[int] = None) -
240
240
  return [f"https://www.youtube.com/watch?v={e['id']}" for e in entries if e.get("id")]
241
241
 
242
242
 
243
- def runSWATPlus(txtinoutDir: str, finalDir: str, executablePath: str = "swatplus", v: bool = True):
243
+ def runSWATPlus(txtinoutDir: str, finalDir: str, executablePath: str = "swatplus", v: bool = True) -> None:
244
244
  os.chdir(txtinoutDir)
245
245
 
246
246
  if not v:
@@ -339,7 +339,7 @@ def formatTimedelta(delta: timedelta) -> str:
339
339
  return f"{time_fmt}"
340
340
 
341
341
 
342
- def setMp3Metadata(fn, metadata, imagePath=None):
342
+ def setMp3Metadata(fn: str, metadata: dict, imagePath: Optional[str] = None) -> bool:
343
343
  '''
344
344
  This function takes a path to an mp3 and a metadata dictionary,
345
345
  then writes that metadata to the file's ID3 tags.
@@ -434,7 +434,7 @@ def deleteFile(filePath:str, v:bool = False) -> bool:
434
434
 
435
435
  return deleted
436
436
 
437
- def removeImageColour(inPath:str, outPath:str, colour:tuple = (255, 255, 255), tolerance:int = 30):
437
+ def removeImageColour(inPath:str, outPath:str, colour:tuple = (255, 255, 255), tolerance:int = 30) -> None:
438
438
  '''
439
439
  Remove a specific color from an image.
440
440
  colour: RGB tuple, e.g., (255, 0, 0) for red
@@ -457,7 +457,7 @@ def removeImageColour(inPath:str, outPath:str, colour:tuple = (255, 255, 255), t
457
457
  img.putdata(new_data)
458
458
  img.save(outPath)
459
459
 
460
- def makeTransparent(inPath:str, outPath:str, colour:tuple = (255, 255, 255), tolerance:int = 30):
460
+ def makeTransparent(inPath:str, outPath:str, colour:tuple = (255, 255, 255), tolerance:int = 30) -> None:
461
461
  '''
462
462
  Make some pixels in an image transparent.
463
463
  '''
@@ -479,9 +479,9 @@ def alert(message:str, server:str = "http://ntfy.sh", topic:str = "pythonAlerts"
479
479
  return: True if the alert was sent successfully, False otherwise
480
480
  '''
481
481
  print(message) if printIt else None; header_data = {}
482
- if not messageTitle is None: header_data["Title"] = messageTitle
483
- if not priority is None: header_data["Priority"] = priority
484
- if not len(tags) == 0: header_data["Tags"] = ",".join(tags)
482
+ if not messageTitle is None: header_data["Title"] = str(messageTitle).replace("\r"," ").replace("\n"," ")
483
+ if not priority is None: header_data["Priority"] = str(int(priority))
484
+ if not len(tags) == 0: header_data["Tags"] = ",".join(map(str, tags))
485
485
 
486
486
  try:
487
487
  if v: print(f"sending alert to {server}/{topic}")
@@ -517,9 +517,10 @@ def deletePath(path:str, v:bool = False) -> bool:
517
517
  if v:
518
518
  print(f'! {path} does not exist')
519
519
  deleted = False
520
+ return deleted
520
521
 
521
522
 
522
- def downloadChunk(url, start, end, path):
523
+ def downloadChunk(url: str, start: int, end: int, path: str) -> None:
523
524
  headers = {'Range': f'bytes={start}-{end}'}
524
525
  response = requests.get(url, headers=headers, stream=True)
525
526
  with open(path, 'wb') as f:
@@ -528,7 +529,47 @@ def downloadChunk(url, start, end, path):
528
529
  f.write(chunk)
529
530
 
530
531
 
531
- def formatStringBlock(input_str, max_chars=70):
532
+ def correctFisheye(inputFile: str, outputFile: str = '',
533
+ k1: float = -0.1, k2: float = 0.05,
534
+ cx: float = 0.5, cy: float = 0.5,
535
+ crf: int = 20) -> str:
536
+ """
537
+ Correct fisheye distortion in a video and save as MP4.
538
+
539
+ Args:
540
+ inputFile (str): Path to the input video (any format).
541
+ outputFile (str, optional): Path for the corrected MP4. If None, auto-creates.
542
+ k1, k2 (float): Lens distortion coefficients.
543
+ cx, cy (float): Optical center (0.5 = image center).
544
+ crf (int): Constant Rate Factor (lower = better quality, larger file).
545
+
546
+ Returns:
547
+ str: Path to the corrected MP4 file.
548
+ """
549
+ if not os.path.exists(inputFile):
550
+ raise FileNotFoundError(f"Input file not found: {inputFile}")
551
+
552
+ # Default output path
553
+ if outputFile == '':
554
+ base, _ = os.path.splitext(inputFile)
555
+ outputFile = f"{base}_corrected.mp4"
556
+
557
+ cmd = [
558
+ "ffmpeg", "-hide_banner", "-loglevel", "error", "-stats",
559
+ "-i", inputFile,
560
+ "-vf", f"lenscorrection=cx={cx}:cy={cy}:k1={k1}:k2={k2}",
561
+ "-c:v", "libx264", "-preset", "slow", f"-crf", str(crf),
562
+ "-pix_fmt", "yuv420p",
563
+ "-c:a", "aac", "-b:a", "192k",
564
+ "-movflags", "+faststart",
565
+ outputFile
566
+ ]
567
+
568
+ subprocess.run(cmd, check=True)
569
+ return outputFile
570
+
571
+
572
+ def formatStringBlock(input_str: str, max_chars: int = 70) -> str:
532
573
  '''
533
574
  This function takes a string and formats it into a block of text
534
575
  with a maximum number of characters per line.
@@ -558,8 +599,7 @@ def formatStringBlock(input_str, max_chars=70):
558
599
 
559
600
 
560
601
 
561
-
562
- def downloadFile(url, save_path, exists_action='resume', num_connections=5, v=False):
602
+ def downloadFile(url: str, save_path: str, exists_action: str = 'resume', num_connections: int = 5, v: bool = False) -> None:
563
603
  if v:
564
604
  print(f"\ndownloading {url}")
565
605
  fname = getFileBaseName(url, extension=True)
@@ -573,13 +613,17 @@ def downloadFile(url, save_path, exists_action='resume', num_connections=5, v=Fa
573
613
  if os.path.exists(save_fname):
574
614
  if exists_action == 'skip':
575
615
  if v:
576
- print(f"File exists, skipping: {save_fname}")
616
+ print(f"file exists, skipping: {save_fname}")
577
617
  return
578
618
  elif exists_action == 'overwrite':
579
619
  os.remove(save_fname)
580
620
  # 'resume' is handled below
581
621
 
582
- # Get file size
622
+ # Get file size (suppress urllib3 warnings when v=False)
623
+ import urllib3
624
+ if not v:
625
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
626
+
583
627
  response = requests.head(url)
584
628
  file_size = int(response.headers.get('content-length', 0))
585
629
 
@@ -589,7 +633,7 @@ def downloadFile(url, save_path, exists_action='resume', num_connections=5, v=Fa
589
633
  initial_pos = os.path.getsize(save_fname)
590
634
  if initial_pos >= file_size:
591
635
  if v:
592
- print(f"File already completed: {save_fname}")
636
+ print(f"file already completed: {save_fname}")
593
637
  return
594
638
 
595
639
  # Calculate chunk sizes
@@ -609,14 +653,22 @@ def downloadFile(url, save_path, exists_action='resume', num_connections=5, v=Fa
609
653
  executor.submit(downloadChunk, url, start, end, temp_files[i])
610
654
  )
611
655
 
612
- # Wait for all downloads to complete with progress bar
613
- with tqdm(total=file_size-initial_pos, initial=initial_pos, unit='B',
614
- unit_scale=True, desc=fname) as pbar:
615
- completed = initial_pos
616
- while completed < file_size:
656
+ # Wait for all downloads to complete with progress bar (conditionally show progress)
657
+ if v:
658
+ with tqdm(total=file_size-initial_pos, initial=initial_pos, unit='B',
659
+ unit_scale=True, desc=fname) as pbar:
660
+ completed = initial_pos
661
+ while completed < file_size:
662
+ current = sum(os.path.getsize(f) for f in temp_files if os.path.exists(f))
663
+ pbar.update(current - completed)
664
+ completed = current
665
+ else:
666
+ # Wait silently without progress bar
667
+ while True:
617
668
  current = sum(os.path.getsize(f) for f in temp_files if os.path.exists(f))
618
- pbar.update(current - completed)
619
- completed = current
669
+ if current >= file_size - initial_pos:
670
+ break
671
+ time.sleep(0.1)
620
672
 
621
673
  # Merge chunks
622
674
  with open(save_fname, 'ab' if initial_pos > 0 else 'wb') as outfile:
@@ -627,6 +679,7 @@ def downloadFile(url, save_path, exists_action='resume', num_connections=5, v=Fa
627
679
  os.remove(temp_file)
628
680
 
629
681
 
682
+
630
683
  def mergeRasterTiles(tileList:list, outFile:str) -> str:
631
684
  '''
632
685
  Merge raster tiles into one raster file
@@ -649,7 +702,7 @@ def systemPlatform() -> str:
649
702
  '''
650
703
  return platform.system()
651
704
 
652
- def progressBar(count, total, message=""):
705
+ def progressBar(count: int, total: int, message: str = "") -> None:
653
706
  percent = int(count / total * 100)
654
707
  filled = int(percent / 2)
655
708
  bar = '█' * filled + '░' * (50 - filled)
@@ -669,7 +722,7 @@ def fileCount(path:str = "./", extension:str = ".*", v:bool = True) -> int:
669
722
  print(f'> there are {count} {extension if not extension ==".*" else ""} files in {path}')
670
723
  return count
671
724
 
672
- def resampleRaster(inFile:str, outFile:str, resolution:float, dstSRS = None, resamplingMethod = 'bilinear', replaceOutput:bool = True, v:bool = True) -> str:
725
+ def resampleRaster(inFile:str, outFile:str, resolution:float, dstSRS = None, resamplingMethod = 'bilinear', replaceOutput:bool = True, v:bool = True) -> Optional[str]:
673
726
  '''
674
727
  Resample a raster file
675
728
  inFile: input raster file
@@ -740,7 +793,7 @@ def watchFileCount(path:str="./", extension:str = ".*", interval:float = 0.2, du
740
793
  return None
741
794
 
742
795
 
743
- def pythonVariable(filename, option, variable=None):
796
+ def pythonVariable(filename: str, option: str, variable: Any = None) -> Any:
744
797
  '''
745
798
  option: save, load or open
746
799
 
@@ -774,7 +827,7 @@ def listFolders(path:str) -> list:
774
827
  else:
775
828
  return []
776
829
 
777
- def readFrom(filename, decode_codec = None, v=False):
830
+ def readFrom(filename: str, decode_codec: Optional[str] = None, v: bool = False) -> Any:
778
831
  '''
779
832
  a function to read ascii files
780
833
  '''
@@ -793,16 +846,16 @@ def readFrom(filename, decode_codec = None, v=False):
793
846
 
794
847
 
795
848
  def pointsToGeodataframe(
796
- rowList,
797
- columnNames,
798
- latIndex,
799
- lonIndex,
800
- auth = "EPSG",
801
- code = "4326",
802
- outShape = "",
803
- format = "gpkg",
804
- v = False,
805
- includeLatLon = True ) -> geopandas.GeoDataFrame:
849
+ rowList: list,
850
+ columnNames: list,
851
+ latIndex: int,
852
+ lonIndex: int,
853
+ auth: str = "EPSG",
854
+ code: str = "4326",
855
+ outShape: str = "",
856
+ format: str = "gpkg",
857
+ v: bool = False,
858
+ includeLatLon: bool = True ) -> geopandas.GeoDataFrame:
806
859
  df = pandas.DataFrame(rowList, columns = columnNames)
807
860
  geometry = [
808
861
  Point(row[lonIndex], row[latIndex]) for row in rowList
@@ -824,10 +877,10 @@ def pointsToGeodataframe(
824
877
  return gdf
825
878
 
826
879
 
827
- def readFile(filename, decode_codec = None, v=False):
880
+ def readFile(filename: str, decode_codec: Optional[str] = None, v: bool = False) -> Any:
828
881
  return readFrom(filename, decode_codec, v)
829
882
 
830
- def writeTo(filename, file_text, encode_codec = None, v=False) -> bool:
883
+ def writeTo(filename: str, file_text: Any, encode_codec: Optional[str] = None, v: bool = False) -> bool:
831
884
  '''
832
885
  a function to write ascii files
833
886
  '''
@@ -846,13 +899,13 @@ def writeTo(filename, file_text, encode_codec = None, v=False) -> bool:
846
899
  if v: print("\t> wrote {0}".format(getFileBaseName(filename, extension=True)))
847
900
  return True
848
901
 
849
- def writeToFile(filename, file_text, encode_codec = None, v=False) -> bool:
902
+ def writeToFile(filename: str, file_text: Any, encode_codec: Optional[str] = None, v: bool = False) -> bool:
850
903
  return writeTo(filename, file_text, encode_codec, v)
851
904
 
852
- def writeFile(filename, file_text, encode_codec = None, v=False) -> bool:
905
+ def writeFile(filename: str, file_text: Any, encode_codec: Optional[str] = None, v: bool = False) -> bool:
853
906
  return writeTo(filename, file_text, encode_codec, v)
854
907
 
855
- def createPath(pathName, v = False):
908
+ def createPath(pathName: str, v: bool = False) -> str:
856
909
  '''
857
910
  this function creates a directory if it does not exist
858
911
  pathName: the path to create
@@ -871,7 +924,7 @@ def createPath(pathName, v = False):
871
924
  return pathName
872
925
 
873
926
 
874
- def renameNetCDFvariable(input_file: str, output_file: str, old_var_name: str, new_var_name: str, v = False) -> None:
927
+ def renameNetCDFvariable(input_file: str, output_file: str, old_var_name: str, new_var_name: str, v: bool = False) -> None:
875
928
  """
876
929
  Renames a variable in a NetCDF file using CDO if it exists.
877
930
  If the variable does not exist, the file is copied without modification.
@@ -907,7 +960,7 @@ def renameNetCDFvariable(input_file: str, output_file: str, old_var_name: str, n
907
960
  print(f"Error: {e.stderr}")
908
961
 
909
962
 
910
- def compressTo7z(input_dir: str, output_file: str, compressionLevel: int = 4, excludeExt: list = None, v: bool = False) -> None:
963
+ def compressTo7z(input_dir: str, output_file: str, compressionLevel: int = 4, excludeExt: Optional[list] = None, v: bool = False) -> None:
911
964
  """
912
965
  Compresses the contents of a directory to a .7z archive with maximum compression.
913
966
 
@@ -1210,8 +1263,8 @@ def ignoreWarnings(ignore:bool = True, v:bool = False) -> None:
1210
1263
  return None
1211
1264
 
1212
1265
 
1213
- def createGrid(topLeft: list = None, bottomRight: list = None, resolution: float = None,
1214
- inputShape: str = None, crs: str = "EPSG:4326", saveVector: str = None) -> geopandas.GeoDataFrame:
1266
+ def createGrid(topLeft: Optional[list] = None, bottomRight: Optional[list] = None, resolution: Optional[float] = None,
1267
+ inputShape: Optional[str] = None, crs: str = "EPSG:4326", saveVector: Optional[str] = None) -> geopandas.GeoDataFrame:
1215
1268
  '''
1216
1269
  This function creates a grid of polygons based on either a shapefile or corner coordinates
1217
1270
 
@@ -1341,7 +1394,7 @@ def netcdfVariableDimensions(ncFile: str, variable: str) -> dict:
1341
1394
 
1342
1395
  return bands_info
1343
1396
 
1344
- def netcdfExportTif(ncFile: str, variable: str, outputFile: Optional[str] = None, band: int = None, v:bool = True) -> gdal.Dataset:
1397
+ def netcdfExportTif(ncFile: str, variable: str, outputFile: Optional[str] = None, band: Optional[int] = None, v:bool = True) -> gdal.Dataset:
1345
1398
  '''
1346
1399
  Export a variable from a NetCDF file to a GeoTiff file
1347
1400
  ncFile: NetCDF file
@@ -1401,8 +1454,8 @@ def netcdfSumMaps(ncFiles:list, variable:str, band:int = 1) -> numpy.ndarray:
1401
1454
  def tiffWriteArray(array: numpy.ndarray, outputFile: str,
1402
1455
  geoTransform: tuple = (0, 1, 0, 0, 0, -1),
1403
1456
  projection: str = 'EPSG:4326',
1404
- noData:float = None,
1405
- v:bool = False) -> gdal.Dataset:
1457
+ noData: Optional[float] = None,
1458
+ v: bool = False) -> gdal.Dataset:
1406
1459
  '''
1407
1460
  Write a numpy array to a GeoTIFF file
1408
1461
  array : numpy array to write
@@ -1448,7 +1501,7 @@ def copyFile(source:str, destination:str, v:bool = True) -> None:
1448
1501
  if v: print(f'> {source} copied to \t - {destination}')
1449
1502
 
1450
1503
 
1451
- def copyDirectory(source:str, destination:str, recursive = True, v:bool = True, filter = []) -> None:
1504
+ def copyDirectory(source:str, destination:str, recursive: bool = True, v:bool = True, filter: list = []) -> None:
1452
1505
  '''
1453
1506
  Copy a directory from source to destination
1454
1507
  source: source directory
@@ -1499,7 +1552,7 @@ def copyFolder(source:str, destination:str, v:bool = True) -> None:
1499
1552
  copyDirectory(source, destination, v=v)
1500
1553
 
1501
1554
 
1502
- def convertCoordinates(lon, lat, srcEPSG, dstCRS) -> tuple:
1555
+ def convertCoordinates(lon: float, lat: float, srcEPSG: str, dstCRS: str) -> tuple:
1503
1556
  """
1504
1557
  this function converts coordinates from one CRS to another
1505
1558
 
@@ -1516,7 +1569,7 @@ def convertCoordinates(lon, lat, srcEPSG, dstCRS) -> tuple:
1516
1569
  return (new_lon, new_lat)
1517
1570
 
1518
1571
 
1519
- def extractRasterValue(rasterPath: str, lat: float, lon: float, coordProj: str = 'EPSG:4326') -> float:
1572
+ def extractRasterValue(rasterPath: str, lat: float, lon: float, coordProj: str = 'EPSG:4326') -> Optional[float]:
1520
1573
  """
1521
1574
  Extract raster value at given coordinates.
1522
1575
 
@@ -1561,7 +1614,7 @@ def extractRasterValue(rasterPath: str, lat: float, lon: float, coordProj: str =
1561
1614
  return float(value)
1562
1615
 
1563
1616
 
1564
- def getRasterValue(rasterPath: str, lat: float, lon: float, coordProj: str = 'EPSG:4326') -> float:
1617
+ def getRasterValue(rasterPath: str, lat: float, lon: float, coordProj: str = 'EPSG:4326') -> Optional[float]:
1565
1618
  '''
1566
1619
  this function is a wrapper for extractRasterValue
1567
1620
  '''
@@ -1588,9 +1641,9 @@ def showProgress(count: int, end: int, message: str, barLength: int = 100) -> No
1588
1641
  percentStr = f'{percent:03.1f}'
1589
1642
  filled = int(barLength * count / end)
1590
1643
  bar = '█' * filled + '░' * (barLength - filled)
1591
- print(f'\r{bar} {percentStr}% [{count}/{end}] | {message} ', end='', flush=True)
1644
+ print(f'\r{message} |{bar}| {percentStr}% [{count}/{end}]', end='', flush=True)
1592
1645
  if count == end:
1593
- print(f'\r{bar} {percentStr}% [{count}/{end}] ', end='', flush=True)
1646
+ print(f'\r{message} |{bar}| {percentStr}% [{count}/{end}] ', end='', flush=True)
1594
1647
  print()
1595
1648
 
1596
1649
 
@@ -1621,7 +1674,7 @@ def dualProgress(primaryCount: int, primaryEnd: int,
1621
1674
  print(f'\r{bar} {formattedPrimaryPercent.rjust(6)}% | {formattedSecondaryPercent.rjust(6)}% ', end='', flush=True)
1622
1675
 
1623
1676
 
1624
- def listAllFiles(folder, extension="*"):
1677
+ def listAllFiles(folder: str, extension: str = "*") -> list:
1625
1678
  list_of_files = []
1626
1679
  # Getting the current work directory (cwd)
1627
1680
  thisdir = folder
@@ -1643,7 +1696,7 @@ def listAllFiles(folder, extension="*"):
1643
1696
  return list_of_files
1644
1697
 
1645
1698
 
1646
- def clipFeatures(inputFeaturePath:str, boundaryFeature:str, outputFeature:str, keepOnlyTypes = None, v = False) -> geopandas.GeoDataFrame:
1699
+ def clipFeatures(inputFeaturePath:str, boundaryFeature:str, outputFeature:str, keepOnlyTypes: Optional[list] = None, v: bool = False) -> geopandas.GeoDataFrame:
1647
1700
  '''
1648
1701
  keepOnlyTypes = ['MultiPolygon', 'Polygon', 'Point', etc]
1649
1702
 
@@ -2023,7 +2076,7 @@ def getTimeseriesStats(data:pandas.DataFrame, observed:Optional[str] = None, sim
2023
2076
 
2024
2077
  return stats
2025
2078
 
2026
- def readSWATPlusOutputs(filePath: str, column: Optional[str] = None, unit: Optional[int] = None, gis_id: Optional[int] = None, name: Optional[str] = None):
2079
+ def readSWATPlusOutputs(filePath: str, column: Optional[str] = None, unit: Optional[int] = None, gis_id: Optional[int] = None, name: Optional[str] = None) -> Optional[pandas.DataFrame]:
2027
2080
  '''
2028
2081
  Read SWAT+ output files and return a pandas DataFrame with proper date handling
2029
2082
  and optional filtering capabilities.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ccfx
3
- Version: 1.0.7
3
+ Version: 1.0.9
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ccfx"
7
- version = "1.0.7"
7
+ version = "1.0.9"
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
File without changes
File without changes
File without changes