Metadata-Version: 2.4
Name: tempolocus
Version: 1.2.0
Summary: Guess likely timezone or region from temporal JSON activity.
Author: tempolocus contributors
License: AGPL-3.0-or-later
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# tempolocus

tempolocus looks at time-series activity patterns to infer a location.

<img width="1297" height="937" alt="Tempolocus used in AIL Project" src="https://github.com/user-attachments/assets/942d1a3a-768d-491f-aa0a-ab39421a13a0" />


## Using tempolocus 

`tempolocus` accepts two JSON shapes plus timestamp-list imports:

- Weekly hourly buckets: a list of objects containing `day`, `hour`, and `count`.
- Yearly daily buckets: an object containing `year`, `max`, and `nb`, where `nb`
  is a list of `[YYYY-MM-DD, count]` pairs.
- Timestamp lists: either a JSON list of UTC timestamp strings / Unix epoch
  seconds, or a plain text file with one timestamp per line. This is useful for
  PE `TimeDateStamp` / compiled-timestamp values and other event lists that
  should be aggregated into weekly patterns.

Run it from the repository:

```bash
python -m tempolocus samples/weekfull-chan1.json
python -m tempolocus samples/year.json --format text
```

Or install the package locally:

```bash
python -m pip install -e .
tempolocus samples/year-chan1.json --top 10
tempolocus samples/year.json --holiday-profile public-worker --format text
tempolocus samples/year.json --activity-signal peak --format text
tempolocus timestamps.txt --kind timestamps --format text
tempolocus timestamps.txt --start-date 2026-01-01 --end-date 2026-03-31
```

Timestamp strings may use ISO-8601 forms such as `2026-01-05T09:15:00Z`
or `2026-01-05 09:15:00 UTC`; numeric entries are interpreted as Unix epoch
seconds in UTC. The output is probabilistic JSON and includes a generic `analysis.activity_type`
classification of `work-time`, `vacation-time`, or `mixed-time`. Weekly inputs
rank timezone offsets, representative IANA zones, and a `probable_countries`
list that highlights countries whose multiple timezones appear in the top
timezone-offset results. Yearly inputs rank broad regions by comparing
activity on public-holiday calendars.

Large timestamp or yearly inputs can be restricted before analysis with the
inclusive `--start-date YYYY-MM-DD` and `--end-date YYYY-MM-DD` options. Either
boundary may be omitted. Date filtering is unavailable for weekly buckets,
because that input shape contains weekdays and hours but no calendar dates.

### Supported yearly holiday regions

| Area | Supported regions |
| --- | --- |
| 🌍 Africa | 🇩🇿 Algeria, 🇨🇮 Côte d'Ivoire, 🇪🇬 Egypt, 🇪🇹 Ethiopia, 🇬🇭 Ghana, 🇰🇪 Kenya, 🇲🇦 Morocco, 🇳🇬 Nigeria, 🇸🇳 Senegal, 🇿🇦 South Africa, 🇹🇳 Tunisia |
| 🌏 Asia-Pacific | 🇦🇺 Australia, 🇨🇳 China, 🇮🇳 India, 🇯🇵 Japan, 🇲🇾 Malaysia, 🇳🇿 New Zealand, 🇰🇵 North Korea, 🇵🇭 Philippines, 🇸🇬 Singapore, 🇰🇷 South Korea, 🇹🇭 Thailand, 🇻🇳 Vietnam |
| 🇪🇺 Europe | 🇦🇹 Austria, 🇧🇪 Belgium, 🇧🇦 Bosnia and Herzegovina, 🇧🇬 Bulgaria, 🇭🇷 Croatia, 🇨🇿 Czechia, 🇩🇰 Denmark, 🇪🇪 Estonia, 🇫🇮 Finland, 🇫🇷 France, 🇩🇪 Germany, 🇬🇷 Greece, 🇭🇺 Hungary, 🇮🇸 Iceland, 🇮🇪 Ireland, 🇮🇹 Italy, 🇱🇻 Latvia, 🇱🇹 Lithuania, 🇱🇺 Luxembourg, 🇳🇱 Netherlands, 🇳🇴 Norway, 🇵🇱 Poland, 🇵🇹 Portugal, 🇷🇴 Romania, 🇷🇺 Russia, 🇷🇸 Serbia, 🇸🇰 Slovakia, 🇸🇮 Slovenia, 🇪🇸 Spain, 🇸🇪 Sweden, 🇨🇭 Switzerland, 🇺🇦 Ukraine, 🇬🇧 United Kingdom |
| 🌎 North America & Caribbean | 🇨🇦 Canada, 🇨🇷 Costa Rica, 🇨🇺 Cuba, 🇩🇴 Dominican Republic, 🇬🇹 Guatemala, 🇲🇽 Mexico, 🇵🇦 Panama, 🇺🇸 United States |
| 🕌 Middle East | 🇧🇭 Bahrain, 🇮🇱 Israel, 🇯🇴 Jordan, 🇰🇼 Kuwait, 🇱🇧 Lebanon, 🇴🇲 Oman, 🇶🇦 Qatar, 🇸🇦 Saudi Arabia, 🇦🇪 United Arab Emirates |
| 🌎 South America | 🇦🇷 Argentina, 🇧🇷 Brazil, 🇨🇱 Chile, 🇨🇴 Colombia, 🇵🇪 Peru, 🇺🇾 Uruguay |

Yearly analysis treats a lack of activity on holidays as the default signal;
pass `--activity-signal peak` when unusually high activity is the indicator you
want to match instead. Yearly analysis defaults to standard public holidays;
pass `--holiday-profile public-worker` to add public-sector worker references,
such as state-worker, Golden Week, bridge-day, or administrative closure days,
alongside standard holidays. The public-worker profile includes additional
China and Russia references for government and public-sector closure patterns,
and South American public-servant references with common administrative bridge
or year-end closure days.

The calendars include recent statutory changes such as Ireland's Saint Brigid's
Day, Poland's Christmas Eve holiday, Italy's Saint Francis of Assisi Day from
2026, and Ukraine's revised holiday dates. Published New Zealand Matariki dates
are covered through 2052, while the explicit East Asian lunar reference tables
currently cover Chinese New Year and Chuseok through 2035. Islamic dates are
calendar estimates and can differ by a day from an official local moon-sighting
announcement; regional and one-off government closure announcements should
therefore be treated as reference signals rather than payroll calendars.

The generic activity analysis compares weekly business-hours against
weekend/off-hours activity, or yearly weekday activity against weekend activity.
It is intended as a broad activity-label heuristic rather than a declaration of
why the activity occurred.

This is a heuristic first pass. Weekly data cannot uniquely identify an IANA
timezone without dates, and yearly data is sensitive to the meaning of the
activity counter.

## Using tempolocus as a Python library

Install `tempolocus` in the Python environment that will import it:

```bash
python -m pip install tempolocus
```

For development against a local checkout, install it in editable mode instead:

```bash
python -m pip install -e .
```

The public package entry points are `detect`, `analyze_activity`, and
`load_json`:

```python
from tempolocus import analyze_activity, detect, load_json

data = load_json("samples/weekfull-chan1.json")
result = detect(data, top=10)

print(result["input_type"])
print(result["results"][0]["label"])
print(result["analysis"]["activity_type"])

activity = analyze_activity(data)
print(activity["activity_type"])
```

### `detect(data, kind="auto", top=5, holiday_profile="standard", activity_signal="lack", start_date=None, end_date=None)`

Use `detect` when you want the full inference result. It accepts already-loaded
Python data structures rather than file paths, which makes it suitable for web
services, notebooks, pipelines, and tests. The return value is a dictionary with
metadata, assumptions, signal summaries, and ranked `results`.

Parameters:

- `data`: one of the supported input shapes described below.
- `kind`: `"auto"`, `"weekly"`, `"yearly"`, or `"timestamps"`. Use `"auto"`
  when the input shape is unambiguous; force a kind when your caller already
  knows what it provided.
- `top`: number of ranked candidates to include. Must be at least `1`.
- `holiday_profile`: for yearly inputs, `"standard"` or `"public-worker"`. The
  public-worker profile adds public-sector closure references where available.
- `activity_signal`: for yearly inputs, `"lack"` to match low activity on
  holidays or `"peak"` to match unusually high activity on holidays.
- `start_date` and `end_date`: optional inclusive `YYYY-MM-DD` strings (or
  `datetime.date` values) used to pre-filter timestamp and yearly inputs before
  inference. Weekly buckets cannot be date-filtered because they carry no date.

Weekly hourly bucket example:

```python
from tempolocus import detect

weekly_rows = [
    {"day": day, "hour": hour, "count": 10 if day <= 4 and 9 <= hour <= 17 else 1}
    for day in range(7)
    for hour in range(24)
]

result = detect(weekly_rows, kind="weekly", top=3)
for candidate in result["results"]:
    print(candidate["probability"], candidate["id"], candidate["label"])
```

Timestamp list example:

```python
from tempolocus import detect

timestamps = [
    "2026-01-05T09:15:00Z",
    "2026-01-06 10:30:00 UTC",
    1767605400,  # Unix epoch seconds in UTC
]

result = detect(
    timestamps,
    kind="timestamps",
    start_date="2026-01-01",
    end_date="2026-03-31",
)
print(result["signals"]["timestamps_seen"])
```

Yearly daily bucket example:

```python
from tempolocus import detect

yearly = {
    "year": 2026,
    "max": 42,
    "nb": [
        ["2026-01-01", 0],
        ["2026-01-02", 18],
        ["2026-01-03", 21],
    ],
}

result = detect(
    yearly,
    kind="yearly",
    top=5,
    holiday_profile="public-worker",
    activity_signal="lack",
)
print(result["results"][0]["id"], result["results"][0]["label"])
```

### `analyze_activity(data, kind="auto", start_date=None, end_date=None)`

Use `analyze_activity` when you only need the generic activity classification
without timezone or holiday-region rankings. It returns fields such as
`activity_type`, `score`, and `shares`. Weekly inputs are classified from local
business-hours versus weekend/off-hours activity; yearly inputs compare weekday
and weekend activity. Its optional date boundaries have the same inclusive
semantics as `detect`.

```python
from tempolocus import analyze_activity, load_json

activity = analyze_activity(load_json("samples/year.json"), kind="yearly")
print(activity["activity_type"], activity["score"])
```

### Input shape reference

| Kind | Python shape | Notes |
| --- | --- | --- |
| `weekly` | `list[dict]` with `day`, `hour`, and `count` | `day` is `0` through `6`; `hour` is `0` through `23`; buckets are interpreted as UTC. |
| `yearly` | `dict` with `year`, `max`, and `nb` | `nb` is a list of `[YYYY-MM-DD, count]` pairs. Missing days inside the observed range are filled as zero activity. |
| `timestamps` | `list[str \| int \| float]` | Strings are parsed as UTC timestamps; numbers are Unix epoch seconds in UTC. |

Invalid inputs raise `tempolocus.core.DetectionError`, a subclass of
`ValueError`. Catch it around user-supplied data if you need to return a custom
error response:

```python
from tempolocus import detect
from tempolocus.core import DetectionError

try:
    result = detect(user_supplied_data)
except DetectionError as error:
    result = {"error": str(error)}
```

## Example 

~~~
adulau@blakley:~/git/tempolocus$ python3 -m tempolocus samples/weekfull-chan1.json --format text  -n 10 --holiday-profile public-worker 
input_type: weekly_timeseries
confidence: 0.220
activity_type: mixed-time (0.009)
assumptions:
  - Hourly buckets are interpreted as UTC; timezone candidates are offsets that make the activity look locally human.
  - Weekly data cannot distinguish all IANA zones sharing the same offset, and daylight saving time is not inferable without dates.
probable_countries:
  0.970  Russia (UTC+02:00, UTC+03:00, UTC+04:00, UTC+05:00, UTC+06:00, UTC+07:00, UTC+08:00)
  0.009  France (UTC+01:00, UTC+03:00, UTC+04:00)
  0.008  Kazakhstan (UTC+05:00, UTC+06:00)
  0.003  United Kingdom (UTC+00:00, UTC+06:00)
  0.002  Mongolia (UTC+07:00, UTC+08:00)
results:
  0.208  timezone: UTC+05 Pakistan / western Central Asia
          utc_quiet_window=19:00-01:00; local_quiet_window=00:00-06:00; quiet_activity_ratio=0.366; local_quiet_center=2.5
  0.196  timezone: UTC+04 Gulf / Caucasus
          utc_quiet_window=19:00-01:00; local_quiet_window=23:00-05:00; quiet_activity_ratio=0.366; local_quiet_center=1.5
  0.143  timezone: UTC+06 Bangladesh / central Asia
          utc_quiet_window=19:00-01:00; local_quiet_window=01:00-07:00; quiet_activity_ratio=0.366; local_quiet_center=3.5
  0.129  timezone: UTC+03 East Africa / Arabia / Moscow
          utc_quiet_window=19:00-01:00; local_quiet_window=22:00-04:00; quiet_activity_ratio=0.366; local_quiet_center=0.5
  0.072  timezone: UTC+02 Eastern Europe / southern Africa
          utc_quiet_window=19:00-01:00; local_quiet_window=21:00-03:00; quiet_activity_ratio=0.366; local_quiet_center=23.5
  0.067  timezone: UTC+07 mainland Southeast Asia
          utc_quiet_window=19:00-01:00; local_quiet_window=02:00-08:00; quiet_activity_ratio=0.366; local_quiet_center=4.5
  0.039  timezone: UTC+01 Central Europe / West Africa
          utc_quiet_window=19:00-01:00; local_quiet_window=20:00-02:00; quiet_activity_ratio=0.366; local_quiet_center=22.5
  0.027  timezone: UTC+08 China / Singapore / Western Australia
          utc_quiet_window=19:00-01:00; local_quiet_window=03:00-09:00; quiet_activity_ratio=0.366; local_quiet_center=5.5
  0.023  timezone: UTC+00 Western Europe / West Africa
          utc_quiet_window=19:00-01:00; local_quiet_window=19:00-01:00; quiet_activity_ratio=0.366; local_quiet_center=21.5
  0.015  timezone: UTC-01 Azores / Cape Verde
          utc_quiet_window=19:00-01:00; local_quiet_window=18:00-00:00; quiet_activity_ratio=0.366; local_quiet_center=20.5
~~~

## License

This project is licensed under the GNU Affero General Public License v3.0 or
later. See [LICENSE](LICENSE) for details.
