pycancensus 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,52 @@
1
+ """
2
+ pycancensus: Access, retrieve, and work with Canadian Census data and geography.
3
+
4
+ This package provides integrated, convenient, and uniform access to Canadian
5
+ Census data and geography retrieved using the CensusMapper API.
6
+ """
7
+
8
+ __version__ = "0.1.0"
9
+ __author__ = "Dmitry Shkolnik"
10
+ __email__ = "shkolnikd@gmail.com"
11
+
12
+ from .core import get_census
13
+ from .regions import list_census_regions, search_census_regions
14
+ from .vectors import list_census_vectors, search_census_vectors, label_vectors
15
+ from .datasets import list_census_datasets, dataset_attribution
16
+ from .settings import (
17
+ set_api_key,
18
+ get_api_key,
19
+ set_cache_path,
20
+ get_cache_path,
21
+ remove_api_key,
22
+ show_api_key,
23
+ )
24
+ from .geometry import get_census_geometry
25
+ from .cache import list_cache, remove_from_cache, clear_cache
26
+ from .hierarchy import parent_census_vectors, child_census_vectors, find_census_vectors
27
+ from .intersect_geometry import get_intersecting_geometries
28
+
29
+ __all__ = [
30
+ "get_census",
31
+ "list_census_regions",
32
+ "search_census_regions",
33
+ "list_census_vectors",
34
+ "search_census_vectors",
35
+ "label_vectors",
36
+ "list_census_datasets",
37
+ "dataset_attribution",
38
+ "set_api_key",
39
+ "get_api_key",
40
+ "remove_api_key",
41
+ "show_api_key",
42
+ "set_cache_path",
43
+ "get_cache_path",
44
+ "get_census_geometry",
45
+ "list_cache",
46
+ "remove_from_cache",
47
+ "clear_cache",
48
+ "parent_census_vectors",
49
+ "child_census_vectors",
50
+ "find_census_vectors",
51
+ "get_intersecting_geometries",
52
+ ]
pycancensus/cache.py ADDED
@@ -0,0 +1,187 @@
1
+ """
2
+ Caching functionality for pycancensus.
3
+ """
4
+
5
+ import os
6
+ import pickle
7
+ import hashlib
8
+ from pathlib import Path
9
+ from typing import Any, Optional, List
10
+ import pandas as pd
11
+ import geopandas as gpd
12
+
13
+ from .settings import get_cache_path
14
+
15
+
16
+ def get_cached_data(cache_key: str) -> Optional[Any]:
17
+ """
18
+ Retrieve data from cache if it exists.
19
+
20
+ Parameters
21
+ ----------
22
+ cache_key : str
23
+ Unique identifier for the cached data.
24
+
25
+ Returns
26
+ -------
27
+ Any or None
28
+ Cached data if found, None otherwise.
29
+ """
30
+ cache_path = Path(get_cache_path())
31
+ cache_file = cache_path / f"{cache_key}.pkl"
32
+
33
+ if cache_file.exists():
34
+ try:
35
+ with open(cache_file, "rb") as f:
36
+ return pickle.load(f)
37
+ except Exception:
38
+ # If cache file is corrupted, remove it
39
+ cache_file.unlink(missing_ok=True)
40
+
41
+ return None
42
+
43
+
44
+ def cache_data(cache_key: str, data: Any) -> None:
45
+ """
46
+ Cache data to disk.
47
+
48
+ Parameters
49
+ ----------
50
+ cache_key : str
51
+ Unique identifier for the data.
52
+ data : Any
53
+ Data to cache.
54
+ """
55
+ cache_path = Path(get_cache_path())
56
+ cache_path.mkdir(parents=True, exist_ok=True)
57
+
58
+ cache_file = cache_path / f"{cache_key}.pkl"
59
+
60
+ try:
61
+ with open(cache_file, "wb") as f:
62
+ pickle.dump(data, f)
63
+ except Exception as e:
64
+ print(f"Warning: Failed to cache data: {e}")
65
+
66
+
67
+ def list_cache() -> pd.DataFrame:
68
+ """
69
+ List all cached data files.
70
+
71
+ Returns
72
+ -------
73
+ pd.DataFrame
74
+ DataFrame with information about cached files including:
75
+ - cache_key: The cache key
76
+ - file_path: Full path to cached file
77
+ - size_mb: File size in MB
78
+ - created: Creation timestamp
79
+ - modified: Last modification timestamp
80
+
81
+ Examples
82
+ --------
83
+ >>> import pycancensus as pc
84
+ >>> cache_list = pc.list_cache()
85
+ >>> print(cache_list)
86
+ """
87
+ cache_path = Path(get_cache_path())
88
+
89
+ if not cache_path.exists():
90
+ return pd.DataFrame(
91
+ columns=["cache_key", "file_path", "size_mb", "created", "modified"]
92
+ )
93
+
94
+ cache_files = []
95
+
96
+ for cache_file in cache_path.glob("*.pkl"):
97
+ try:
98
+ stat = cache_file.stat()
99
+ cache_files.append(
100
+ {
101
+ "cache_key": cache_file.stem,
102
+ "file_path": str(cache_file),
103
+ "size_mb": round(stat.st_size / (1024 * 1024), 2),
104
+ "created": pd.Timestamp.fromtimestamp(stat.st_ctime),
105
+ "modified": pd.Timestamp.fromtimestamp(stat.st_mtime),
106
+ }
107
+ )
108
+ except Exception:
109
+ continue
110
+
111
+ return pd.DataFrame(cache_files)
112
+
113
+
114
+ def remove_from_cache(
115
+ cache_keys: Optional[List[str]] = None, all_cache: bool = False
116
+ ) -> None:
117
+ """
118
+ Remove items from cache.
119
+
120
+ Parameters
121
+ ----------
122
+ cache_keys : list of str, optional
123
+ Specific cache keys to remove. If None and all_cache=False,
124
+ does nothing.
125
+ all_cache : bool, default False
126
+ If True, removes all cached data.
127
+
128
+ Examples
129
+ --------
130
+ >>> import pycancensus as pc
131
+ >>> # Remove specific cache entries
132
+ >>> pc.remove_from_cache(["regions_CA16", "vectors_CA16"])
133
+ >>>
134
+ >>> # Remove all cache (use with caution!)
135
+ >>> pc.remove_from_cache(all_cache=True)
136
+ """
137
+ cache_path = Path(get_cache_path())
138
+
139
+ if not cache_path.exists():
140
+ print("No cache directory found.")
141
+ return
142
+
143
+ removed_count = 0
144
+
145
+ if all_cache:
146
+ # Remove all .pkl files
147
+ for cache_file in cache_path.glob("*.pkl"):
148
+ try:
149
+ cache_file.unlink()
150
+ removed_count += 1
151
+ except Exception as e:
152
+ print(f"Warning: Failed to remove {cache_file}: {e}")
153
+
154
+ print(f"Removed {removed_count} cached files.")
155
+
156
+ elif cache_keys:
157
+ # Remove specific cache keys
158
+ for cache_key in cache_keys:
159
+ cache_file = cache_path / f"{cache_key}.pkl"
160
+ if cache_file.exists():
161
+ try:
162
+ cache_file.unlink()
163
+ removed_count += 1
164
+ print(f"Removed cache: {cache_key}")
165
+ except Exception as e:
166
+ print(f"Warning: Failed to remove {cache_key}: {e}")
167
+ else:
168
+ print(f"Cache key not found: {cache_key}")
169
+
170
+ if removed_count > 0:
171
+ print(f"Removed {removed_count} cached files.")
172
+ else:
173
+ print("No cache keys specified and all_cache=False. Nothing to remove.")
174
+
175
+
176
+ def clear_cache() -> None:
177
+ """
178
+ Clear all cached data.
179
+
180
+ This is an alias for remove_from_cache(all_cache=True).
181
+
182
+ Examples
183
+ --------
184
+ >>> import pycancensus as pc
185
+ >>> pc.clear_cache()
186
+ """
187
+ remove_from_cache(all_cache=True)
pycancensus/cli.py ADDED
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Command-line interface for pycancensus.
4
+ """
5
+
6
+ import argparse
7
+ import sys
8
+ import json
9
+ from typing import Optional
10
+
11
+ import pycancensus as pc
12
+
13
+
14
+ def main():
15
+ """Main CLI entry point."""
16
+ parser = argparse.ArgumentParser(
17
+ description="Access Canadian Census data through the CensusMapper API",
18
+ formatter_class=argparse.RawDescriptionHelpFormatter,
19
+ epilog="""
20
+ Examples:
21
+ pycancensus datasets # List available datasets
22
+ pycancensus regions CA16 # List regions for 2016 Census
23
+ pycancensus vectors CA16 # List vectors for 2016 Census
24
+ pycancensus search-regions Vancouver CA16 # Search for Vancouver regions
25
+ pycancensus search-vectors income CA16 # Search for income vectors
26
+
27
+ Set API key with environment variable: export CANCENSUS_API_KEY=your_key_here
28
+ Or get a free key at: https://censusmapper.ca/users/sign_up
29
+ """,
30
+ )
31
+
32
+ parser.add_argument(
33
+ "--api-key",
34
+ help="CensusMapper API key (or set CANCENSUS_API_KEY environment variable)",
35
+ )
36
+ parser.add_argument("--cache-path", help="Path for caching downloaded data")
37
+ parser.add_argument("--no-cache", action="store_true", help="Disable caching")
38
+ parser.add_argument("--quiet", action="store_true", help="Suppress output messages")
39
+
40
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
41
+
42
+ # Datasets command
43
+ datasets_parser = subparsers.add_parser("datasets", help="List available datasets")
44
+
45
+ # Regions command
46
+ regions_parser = subparsers.add_parser("regions", help="List regions for a dataset")
47
+ regions_parser.add_argument("dataset", help="Dataset identifier (e.g., CA16)")
48
+
49
+ # Vectors command
50
+ vectors_parser = subparsers.add_parser("vectors", help="List vectors for a dataset")
51
+ vectors_parser.add_argument("dataset", help="Dataset identifier (e.g., CA16)")
52
+
53
+ # Search regions command
54
+ search_regions_parser = subparsers.add_parser(
55
+ "search-regions", help="Search for regions"
56
+ )
57
+ search_regions_parser.add_argument("term", help="Search term")
58
+ search_regions_parser.add_argument(
59
+ "dataset", help="Dataset identifier (e.g., CA16)"
60
+ )
61
+ search_regions_parser.add_argument("--level", help="Filter by aggregation level")
62
+
63
+ # Search vectors command
64
+ search_vectors_parser = subparsers.add_parser(
65
+ "search-vectors", help="Search for vectors"
66
+ )
67
+ search_vectors_parser.add_argument("term", help="Search term")
68
+ search_vectors_parser.add_argument(
69
+ "dataset", help="Dataset identifier (e.g., CA16)"
70
+ )
71
+ search_vectors_parser.add_argument("--type", help="Filter by vector type")
72
+
73
+ # Get data command
74
+ get_data_parser = subparsers.add_parser("get-data", help="Get census data")
75
+ get_data_parser.add_argument("dataset", help="Dataset identifier (e.g., CA16)")
76
+ get_data_parser.add_argument(
77
+ "--regions", required=True, help='Regions as JSON (e.g., \'{"CMA": "59933"}\')'
78
+ )
79
+ get_data_parser.add_argument("--vectors", help="Comma-separated list of vector IDs")
80
+ get_data_parser.add_argument("--level", default="Regions", help="Aggregation level")
81
+ get_data_parser.add_argument("--geo", action="store_true", help="Include geometry")
82
+ get_data_parser.add_argument("--output", help="Output file path (CSV or GeoJSON)")
83
+
84
+ args = parser.parse_args()
85
+
86
+ # Set up configuration
87
+ if args.api_key:
88
+ pc.set_api_key(args.api_key)
89
+
90
+ if args.cache_path:
91
+ pc.set_cache_path(args.cache_path)
92
+
93
+ use_cache = not args.no_cache
94
+ quiet = args.quiet
95
+
96
+ try:
97
+ if args.command == "datasets":
98
+ df = pc.list_census_datasets(use_cache=use_cache, quiet=quiet)
99
+ print(df.to_string(index=False))
100
+
101
+ elif args.command == "regions":
102
+ df = pc.list_census_regions(args.dataset, use_cache=use_cache, quiet=quiet)
103
+ print(df.to_string(index=False))
104
+
105
+ elif args.command == "vectors":
106
+ df = pc.list_census_vectors(args.dataset, use_cache=use_cache, quiet=quiet)
107
+ print(df.to_string(index=False))
108
+
109
+ elif args.command == "search-regions":
110
+ df = pc.search_census_regions(
111
+ args.term,
112
+ args.dataset,
113
+ level=args.level,
114
+ use_cache=use_cache,
115
+ quiet=quiet,
116
+ )
117
+ print(df.to_string(index=False))
118
+
119
+ elif args.command == "search-vectors":
120
+ df = pc.search_census_vectors(
121
+ args.term,
122
+ args.dataset,
123
+ type_filter=args.type,
124
+ use_cache=use_cache,
125
+ quiet=quiet,
126
+ )
127
+ print(df.to_string(index=False))
128
+
129
+ elif args.command == "get-data":
130
+ # Parse regions JSON
131
+ try:
132
+ regions = json.loads(args.regions)
133
+ except json.JSONDecodeError:
134
+ print("Error: Invalid JSON format for regions", file=sys.stderr)
135
+ return 1
136
+
137
+ # Parse vectors
138
+ vectors = None
139
+ if args.vectors:
140
+ vectors = [v.strip() for v in args.vectors.split(",")]
141
+
142
+ # Get data
143
+ geo_format = "geopandas" if args.geo else None
144
+
145
+ df = pc.get_census(
146
+ dataset=args.dataset,
147
+ regions=regions,
148
+ vectors=vectors,
149
+ level=args.level,
150
+ geo_format=geo_format,
151
+ use_cache=use_cache,
152
+ quiet=quiet,
153
+ )
154
+
155
+ # Output data
156
+ if args.output:
157
+ if args.output.endswith(".geojson") and hasattr(df, "to_file"):
158
+ df.to_file(args.output, driver="GeoJSON")
159
+ if not quiet:
160
+ print(f"Data saved to {args.output}")
161
+ else:
162
+ df.to_csv(args.output, index=False)
163
+ if not quiet:
164
+ print(f"Data saved to {args.output}")
165
+ else:
166
+ print(df.to_string(index=False))
167
+
168
+ else:
169
+ parser.print_help()
170
+ return 1
171
+
172
+ except Exception as e:
173
+ print(f"Error: {e}", file=sys.stderr)
174
+ if not quiet:
175
+ import traceback
176
+
177
+ traceback.print_exc()
178
+ return 1
179
+
180
+ return 0
181
+
182
+
183
+ if __name__ == "__main__":
184
+ sys.exit(main())