openseries 1.7.8__py3-none-any.whl → 1.8.1__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.
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
 
6
6
  from inspect import stack
7
7
  from pathlib import Path
8
- from typing import TYPE_CHECKING, Callable, cast
8
+ from typing import TYPE_CHECKING, cast
9
9
 
10
10
  from numpy import (
11
11
  append,
@@ -21,7 +21,6 @@ from numpy import (
21
21
  from numpy import (
22
22
  sum as npsum,
23
23
  )
24
- from numpy.typing import NDArray
25
24
  from pandas import (
26
25
  DataFrame,
27
26
  Series,
@@ -33,19 +32,24 @@ from plotly.offline import plot # type: ignore[import-untyped,unused-ignore]
33
32
  from scipy.optimize import minimize # type: ignore[import-untyped,unused-ignore]
34
33
 
35
34
  from .load_plotly import load_plotly_dict
36
- from .series import OpenTimeSeries
37
-
38
- # noinspection PyProtectedMember
39
- from .simulation import _random_generator
40
- from .types import (
35
+ from .owntypes import (
36
+ AtLeastOneFrameError,
41
37
  LiteralLinePlotMode,
42
38
  LiteralMinimizeMethods,
43
39
  LiteralPlotlyJSlib,
44
40
  LiteralPlotlyOutput,
41
+ MixedValuetypesError,
45
42
  ValueType,
46
43
  )
44
+ from .series import OpenTimeSeries
45
+
46
+ # noinspection PyProtectedMember
47
+ from .simulation import _random_generator
47
48
 
48
49
  if TYPE_CHECKING: # pragma: no cover
50
+ from collections.abc import Callable
51
+
52
+ from numpy.typing import NDArray
49
53
  from pydantic import DirectoryPath
50
54
 
51
55
  from .frame import OpenFrame
@@ -91,7 +95,7 @@ def simulate_portfolios(
91
95
  log_ret = copi.tsdf.copy()
92
96
  else:
93
97
  msg = "Mix of series types will give inconsistent results"
94
- raise ValueError(msg)
98
+ raise MixedValuetypesError(msg)
95
99
 
96
100
  log_ret.columns = log_ret.columns.droplevel(level=1)
97
101
 
@@ -127,7 +131,7 @@ def simulate_portfolios(
127
131
 
128
132
 
129
133
  # noinspection PyUnusedLocal
130
- def efficient_frontier( # noqa: C901
134
+ def efficient_frontier(
131
135
  eframe: OpenFrame,
132
136
  num_ports: int = 5000,
133
137
  seed: int = 71,
@@ -175,7 +179,7 @@ def efficient_frontier( # noqa: C901
175
179
  log_ret = copi.tsdf.copy()
176
180
  else:
177
181
  msg = "Mix of series types will give inconsistent results"
178
- raise ValueError(msg)
182
+ raise MixedValuetypesError(msg)
179
183
 
180
184
  log_ret.columns = log_ret.columns.droplevel(level=1)
181
185
 
@@ -199,7 +203,7 @@ def efficient_frontier( # noqa: C901
199
203
  ret = npsum(lg_ret.mean() * weights) * per_in_yr
200
204
  volatility = sqrt(weights.T @ (lg_ret.cov() * per_in_yr @ weights))
201
205
  sr = ret / volatility
202
- return cast(NDArray[float64], array([ret, volatility, sr]))
206
+ return cast("NDArray[float64]", array([ret, volatility, sr]))
203
207
 
204
208
  def _diff_return(
205
209
  lg_ret: DataFrame,
@@ -208,14 +212,14 @@ def efficient_frontier( # noqa: C901
208
212
  poss_return: float,
209
213
  ) -> float64:
210
214
  return cast(
211
- float64,
215
+ "float64",
212
216
  _get_ret_vol_sr(lg_ret=lg_ret, weights=weights, per_in_yr=per_in_yr)[0]
213
217
  - poss_return,
214
218
  )
215
219
 
216
220
  def _neg_sharpe(weights: NDArray[float64]) -> float64:
217
221
  return cast(
218
- float64,
222
+ "float64",
219
223
  _get_ret_vol_sr(
220
224
  lg_ret=log_ret,
221
225
  weights=weights,
@@ -228,7 +232,7 @@ def efficient_frontier( # noqa: C901
228
232
  weights: NDArray[float64],
229
233
  ) -> float64:
230
234
  return cast(
231
- float64,
235
+ "float64",
232
236
  _get_ret_vol_sr(
233
237
  lg_ret=log_ret,
234
238
  weights=weights,
@@ -261,7 +265,7 @@ def efficient_frontier( # noqa: C901
261
265
 
262
266
  for possible_return in frontier_y:
263
267
  cons = cast(
264
- dict[str, str | Callable[[float, NDArray[float64]], float64]],
268
+ "dict[str, str | Callable[[float, NDArray[float64]], float64]]",
265
269
  (
266
270
  {"type": "eq", "fun": _check_sum},
267
271
  {
@@ -413,14 +417,16 @@ def prepare_plot_data(
413
417
  [
414
418
  f"{wgt:.1%} {nm}"
415
419
  for wgt, nm in zip(
416
- cast(list[float], assets.weights),
420
+ cast("list[float]", assets.weights),
417
421
  assets.columns_lvl_zero,
422
+ strict=True,
418
423
  )
419
424
  ],
420
425
  )
421
426
 
422
427
  opt_text_list = [
423
- f"{wgt:.1%} {nm}" for wgt, nm in zip(optimized[3:], assets.columns_lvl_zero)
428
+ f"{wgt:.1%} {nm}"
429
+ for wgt, nm in zip(optimized[3:], assets.columns_lvl_zero, strict=True)
424
430
  ]
425
431
  opt_text = "<br><br>Weights:<br>" + "<br>".join(opt_text_list)
426
432
  vol: Series[float] = assets.vol
@@ -442,7 +448,7 @@ def prepare_plot_data(
442
448
  return plotframe
443
449
 
444
450
 
445
- def sharpeplot( # noqa: C901
451
+ def sharpeplot(
446
452
  sim_frame: DataFrame | None = None,
447
453
  line_frame: DataFrame | None = None,
448
454
  point_frame: DataFrame | None = None,
@@ -511,7 +517,7 @@ def sharpeplot( # noqa: C901
511
517
 
512
518
  if sim_frame is None and line_frame is None and point_frame is None:
513
519
  msg = "One of sim_frame, line_frame or point_frame must be provided."
514
- raise ValueError(msg)
520
+ raise AtLeastOneFrameError(msg)
515
521
 
516
522
  if sim_frame is not None:
517
523
  returns.extend(list(sim_frame.loc[:, "ret"]))
@@ -549,10 +555,10 @@ def sharpeplot( # noqa: C901
549
555
 
550
556
  if point_frame is not None:
551
557
  colorway = cast(
552
- dict[str, str | int | float | bool | list[str]],
558
+ "dict[str, str | int | float | bool | list[str]]",
553
559
  fig["layout"],
554
560
  ).get("colorway")[: len(point_frame.columns)]
555
- for col, clr in zip(point_frame.columns, colorway):
561
+ for col, clr in zip(point_frame.columns, colorway, strict=True):
556
562
  returns.extend([point_frame.loc["ret", col]])
557
563
  risk.extend([point_frame.loc["stdev", col]])
558
564
  figure.add_scatter(
@@ -597,7 +603,7 @@ def sharpeplot( # noqa: C901
597
603
  auto_open=auto_open,
598
604
  auto_play=False,
599
605
  link_text="",
600
- include_plotlyjs=cast(bool, include_plotlyjs),
606
+ include_plotlyjs=cast("bool", include_plotlyjs),
601
607
  config=fig["config"],
602
608
  output_type=output_type,
603
609
  )
@@ -608,7 +614,7 @@ def sharpeplot( # noqa: C901
608
614
  fig=figure,
609
615
  config=fig["config"],
610
616
  auto_play=False,
611
- include_plotlyjs=cast(bool, include_plotlyjs),
617
+ include_plotlyjs=cast("bool", include_plotlyjs),
612
618
  full_html=False,
613
619
  div_id=div_id,
614
620
  )
openseries/series.py CHANGED
@@ -1,12 +1,15 @@
1
1
  """Defining the OpenTimeSeries class."""
2
2
 
3
+ # mypy: disable-error-code="no-any-return"
3
4
  from __future__ import annotations
4
5
 
5
- import datetime as dt
6
6
  from collections.abc import Iterable
7
7
  from copy import deepcopy
8
- from logging import warning
9
- from typing import Any, TypeVar, cast
8
+ from logging import getLogger
9
+ from typing import TYPE_CHECKING, Any, TypeVar, cast
10
+
11
+ if TYPE_CHECKING: # pragma: no cover
12
+ import datetime as dt
10
13
 
11
14
  from numpy import (
12
15
  append,
@@ -27,31 +30,35 @@ from pandas import (
27
30
  date_range,
28
31
  )
29
32
  from pydantic import field_validator, model_validator
30
- from typing_extensions import Self
31
33
 
32
34
  from ._common_model import _CommonModel
33
35
  from .datefixer import _do_resample_to_business_period_ends, date_fix
34
- from .types import (
36
+ from .owntypes import (
35
37
  Countries,
36
38
  CountriesType,
37
39
  Currency,
38
40
  CurrencyStringType,
41
+ DateAlignmentError,
39
42
  DateListType,
40
43
  DaysInYearType,
44
+ IncorrectArgumentComboError,
41
45
  LiteralBizDayFreq,
42
46
  LiteralPandasReindexMethod,
43
47
  LiteralSeriesProps,
44
48
  OpenTimeSeriesPropertiesList,
49
+ Self,
45
50
  ValueListType,
46
51
  ValueType,
47
52
  )
48
53
 
54
+ logger = getLogger(__name__)
55
+
49
56
  __all__ = ["OpenTimeSeries", "timeseries_chain"]
50
57
 
51
58
  TypeOpenTimeSeries = TypeVar("TypeOpenTimeSeries", bound="OpenTimeSeries")
52
59
 
53
60
 
54
- # noinspection PyUnresolvedReferences
61
+ # noinspection PyUnresolvedReferences,PyNestedDecorators
55
62
  class OpenTimeSeries(_CommonModel):
56
63
  """OpenTimeSeries objects are at the core of the openseries package.
57
64
 
@@ -142,7 +149,7 @@ class OpenTimeSeries(_CommonModel):
142
149
 
143
150
  @classmethod
144
151
  def from_arrays(
145
- cls: type[OpenTimeSeries],
152
+ cls,
146
153
  name: str,
147
154
  dates: DateListType,
148
155
  values: ValueListType,
@@ -153,7 +160,7 @@ class OpenTimeSeries(_CommonModel):
153
160
  baseccy: CurrencyStringType = "SEK",
154
161
  *,
155
162
  local_ccy: bool = True,
156
- ) -> OpenTimeSeries:
163
+ ) -> Self:
157
164
  """Create series from a Pandas DataFrame or Series.
158
165
 
159
166
  Parameters
@@ -204,14 +211,14 @@ class OpenTimeSeries(_CommonModel):
204
211
 
205
212
  @classmethod
206
213
  def from_df(
207
- cls: type[OpenTimeSeries],
214
+ cls,
208
215
  dframe: Series[float] | DataFrame,
209
216
  column_nmbr: int = 0,
210
217
  valuetype: ValueType = ValueType.PRICE,
211
218
  baseccy: CurrencyStringType = "SEK",
212
219
  *,
213
220
  local_ccy: bool = True,
214
- ) -> OpenTimeSeries:
221
+ ) -> Self:
215
222
  """Create series from a Pandas DataFrame or Series.
216
223
 
217
224
  Parameters
@@ -240,7 +247,7 @@ class OpenTimeSeries(_CommonModel):
240
247
  label, _ = dframe.name
241
248
  else:
242
249
  label = dframe.name
243
- values = cast(list[float], dframe.to_numpy().tolist())
250
+ values = cast("list[float]", dframe.to_numpy().tolist())
244
251
  elif isinstance(dframe, DataFrame):
245
252
  values = dframe.iloc[:, column_nmbr].to_list()
246
253
  if isinstance(dframe.columns, MultiIndex):
@@ -249,7 +256,7 @@ class OpenTimeSeries(_CommonModel):
249
256
  ):
250
257
  label = "Series"
251
258
  msg = f"Label missing. Adding: {label}"
252
- warning(msg=msg)
259
+ logger.warning(msg=msg)
253
260
  else:
254
261
  label = dframe.columns.get_level_values(0).to_numpy()[column_nmbr]
255
262
  if _check_if_none(
@@ -257,13 +264,13 @@ class OpenTimeSeries(_CommonModel):
257
264
  ):
258
265
  valuetype = ValueType.PRICE
259
266
  msg = f"valuetype missing. Adding: {valuetype.value}"
260
- warning(msg=msg)
267
+ logger.warning(msg=msg)
261
268
  else:
262
269
  valuetype = dframe.columns.get_level_values(1).to_numpy()[
263
270
  column_nmbr
264
271
  ]
265
272
  else:
266
- label = cast(MultiIndex, dframe.columns).to_numpy()[column_nmbr]
273
+ label = cast("MultiIndex", dframe.columns).to_numpy()[column_nmbr]
267
274
  else:
268
275
  raise TypeError(msg)
269
276
 
@@ -289,7 +296,7 @@ class OpenTimeSeries(_CommonModel):
289
296
 
290
297
  @classmethod
291
298
  def from_fixed_rate(
292
- cls: type[OpenTimeSeries],
299
+ cls,
293
300
  rate: float,
294
301
  d_range: DatetimeIndex | None = None,
295
302
  days: int | None = None,
@@ -299,7 +306,7 @@ class OpenTimeSeries(_CommonModel):
299
306
  baseccy: CurrencyStringType = "SEK",
300
307
  *,
301
308
  local_ccy: bool = True,
302
- ) -> OpenTimeSeries:
309
+ ) -> Self:
303
310
  """Create series from values accruing with a given fixed rate return.
304
311
 
305
312
  Providing a date_range of type Pandas DatetimeIndex takes priority over
@@ -338,7 +345,7 @@ class OpenTimeSeries(_CommonModel):
338
345
  )
339
346
  elif not isinstance(d_range, Iterable) and not all([days, end_dt]):
340
347
  msg = "If d_range is not provided both days and end_dt must be."
341
- raise ValueError(msg)
348
+ raise IncorrectArgumentComboError(msg)
342
349
 
343
350
  deltas = array(
344
351
  [i.days for i in DatetimeIndex(d_range)[1:] - DatetimeIndex(d_range)[:-1]], # type: ignore[arg-type]
@@ -413,7 +420,7 @@ class OpenTimeSeries(_CommonModel):
413
420
  """
414
421
  if not properties:
415
422
  properties = cast(
416
- list[LiteralSeriesProps],
423
+ "list[LiteralSeriesProps]",
417
424
  OpenTimeSeriesPropertiesList.allowed_strings,
418
425
  )
419
426
 
@@ -435,7 +442,9 @@ class OpenTimeSeries(_CommonModel):
435
442
  returns.iloc[0] = 0
436
443
  self.valuetype = ValueType.RTRN
437
444
  arrays = [[self.label], [self.valuetype]]
438
- returns.columns = MultiIndex.from_arrays(arrays=arrays)
445
+ returns.columns = MultiIndex.from_arrays(
446
+ arrays=arrays # type: ignore[arg-type,unused-ignore]
447
+ )
439
448
  self.tsdf = returns.copy()
440
449
  return self
441
450
 
@@ -513,7 +522,9 @@ class OpenTimeSeries(_CommonModel):
513
522
 
514
523
  deltas = array([i.days for i in self.tsdf.index[1:] - self.tsdf.index[:-1]])
515
524
  # noinspection PyTypeChecker
516
- arr = cumprod(insert(1.0 + deltas * arr[:-1] / days_in_year, 0, 1.0))
525
+ arr = cumprod( # type: ignore[assignment,unused-ignore]
526
+ a=insert(arr=1.0 + deltas * arr[:-1] / days_in_year, obj=0, values=1.0)
527
+ )
517
528
 
518
529
  self.dates = [d.strftime("%Y-%m-%d") for d in self.tsdf.index]
519
530
  self.values = list(arr)
@@ -622,26 +633,26 @@ class OpenTimeSeries(_CommonModel):
622
633
  time_factor = float(periods_in_a_year_fixed)
623
634
  else:
624
635
  how_many = self.tsdf.loc[
625
- cast(int, earlier) : cast(int, later),
636
+ cast("int", earlier) : cast("int", later),
626
637
  self.tsdf.columns.to_numpy()[0],
627
638
  ].count()
628
639
  fraction = (later - earlier).days / 365.25
629
640
  time_factor = how_many / fraction
630
641
 
631
- data = self.tsdf.loc[cast(int, earlier) : cast(int, later)].copy()
642
+ data = self.tsdf.loc[cast("int", earlier) : cast("int", later)].copy()
632
643
 
633
644
  data[self.label, ValueType.RTRN] = (
634
645
  data.loc[:, self.tsdf.columns.to_numpy()[0]].apply(log).diff()
635
646
  )
636
647
 
637
648
  rawdata = [
638
- data.loc[:, cast(int, (self.label, ValueType.RTRN))]
649
+ data.loc[:, cast("int", (self.label, ValueType.RTRN))]
639
650
  .iloc[1:day_chunk]
640
651
  .std(ddof=dlta_degr_freedms)
641
652
  * sqrt(time_factor),
642
653
  ]
643
654
 
644
- for item in data.loc[:, cast(int, (self.label, ValueType.RTRN))].iloc[1:]:
655
+ for item in data.loc[:, cast("int", (self.label, ValueType.RTRN))].iloc[1:]:
645
656
  prev = rawdata[-1]
646
657
  rawdata.append(
647
658
  sqrt(
@@ -682,7 +693,7 @@ class OpenTimeSeries(_CommonModel):
682
693
  values = [1.0]
683
694
  returns_input = True
684
695
  else:
685
- values = [cast(float, self.tsdf.iloc[0, 0])]
696
+ values = [cast("float", self.tsdf.iloc[0, 0])]
686
697
  ra_df = self.tsdf.ffill().pct_change()
687
698
  returns_input = False
688
699
  ra_df = ra_df.dropna()
@@ -691,16 +702,16 @@ class OpenTimeSeries(_CommonModel):
691
702
  dates: list[dt.date] = [prev]
692
703
 
693
704
  for idx, row in ra_df.iterrows():
694
- dates.append(cast(dt.date, idx))
705
+ dates.append(cast("dt.date", idx))
695
706
  values.append(
696
707
  values[-1]
697
708
  * (
698
709
  1
699
710
  + row.iloc[0]
700
- + adjustment * (cast(dt.date, idx) - prev).days / days_in_year
711
+ + adjustment * (cast("dt.date", idx) - prev).days / days_in_year
701
712
  ),
702
713
  )
703
- prev = cast(dt.date, idx)
714
+ prev = cast("dt.date", idx)
704
715
  self.tsdf = DataFrame(data=values, index=dates)
705
716
  self.valuetype = ValueType.PRICE
706
717
  self.tsdf.columns = MultiIndex.from_arrays(
@@ -750,7 +761,7 @@ class OpenTimeSeries(_CommonModel):
750
761
  self.valuetype = lvl_one
751
762
  else:
752
763
  self.tsdf.columns = MultiIndex.from_arrays([[lvl_zero], [lvl_one]])
753
- self.label, self.valuetype = lvl_zero, cast(ValueType, lvl_one)
764
+ self.label, self.valuetype = lvl_zero, cast("ValueType", lvl_one)
754
765
  if delete_lvl_one:
755
766
  self.tsdf.columns = self.tsdf.columns.droplevel(level=1)
756
767
  return self
@@ -760,7 +771,7 @@ def timeseries_chain(
760
771
  front: TypeOpenTimeSeries,
761
772
  back: TypeOpenTimeSeries,
762
773
  old_fee: float = 0.0,
763
- ) -> TypeOpenTimeSeries | OpenTimeSeries:
774
+ ) -> TypeOpenTimeSeries:
764
775
  """Chain two timeseries together.
765
776
 
766
777
  The function assumes that the two series have at least one date in common.
@@ -776,7 +787,7 @@ def timeseries_chain(
776
787
 
777
788
  Returns
778
789
  -------
779
- TypeOpenTimeSeries | OpenTimeSeries
790
+ TypeOpenTimeSeries
780
791
  An OpenTimeSeries object or a subclass thereof
781
792
 
782
793
  """
@@ -788,14 +799,14 @@ def timeseries_chain(
788
799
 
789
800
  if old.last_idx < first:
790
801
  msg = "Timeseries dates must overlap to allow them to be chained."
791
- raise ValueError(msg)
802
+ raise DateAlignmentError(msg)
792
803
 
793
804
  while first not in old.tsdf.index:
794
805
  idx += 1
795
806
  first = new.tsdf.index[idx]
796
807
  if first > old.tsdf.index[-1]:
797
808
  msg = "Failed to find a matching date between series"
798
- raise ValueError(msg)
809
+ raise DateAlignmentError(msg)
799
810
 
800
811
  dates: list[str] = [x.strftime("%Y-%m-%d") for x in old.tsdf.index if x < first]
801
812
 
@@ -807,27 +818,6 @@ def timeseries_chain(
807
818
 
808
819
  dates.extend([x.strftime("%Y-%m-%d") for x in new.tsdf.index])
809
820
 
810
- # noinspection PyUnresolvedReferences
811
- if back.__class__.__subclasscheck__(
812
- OpenTimeSeries,
813
- ):
814
- return OpenTimeSeries(
815
- timeseries_id=new.timeseries_id,
816
- instrument_id=new.instrument_id,
817
- currency=new.currency,
818
- dates=dates,
819
- name=new.name,
820
- label=new.name,
821
- valuetype=new.valuetype,
822
- values=list(values),
823
- local_ccy=new.local_ccy,
824
- tsdf=DataFrame(
825
- data=values,
826
- index=[d.date() for d in DatetimeIndex(dates)],
827
- columns=[[new.label], [new.valuetype]],
828
- dtype="float64",
829
- ),
830
- )
831
821
  return back.__class__(
832
822
  timeseries_id=new.timeseries_id,
833
823
  instrument_id=new.instrument_id,
@@ -862,7 +852,7 @@ def _check_if_none(item: Any) -> bool: # noqa: ANN401
862
852
 
863
853
  """
864
854
  try:
865
- return cast(bool, isnan(item))
855
+ return cast("bool", isnan(item))
866
856
  except TypeError:
867
857
  if item is None:
868
858
  return True
openseries/simulation.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """Defining the ReturnSimulation class."""
2
2
 
3
+ # mypy: disable-error-code="no-any-return"
3
4
  from __future__ import annotations
4
5
 
5
6
  from typing import TYPE_CHECKING, cast
@@ -22,12 +23,12 @@ from pydantic import (
22
23
  PositiveFloat,
23
24
  PositiveInt,
24
25
  )
25
- from typing_extensions import Self
26
26
 
27
27
  from .datefixer import generate_calendar_date_range
28
- from .types import (
28
+ from .owntypes import (
29
29
  CountriesType,
30
30
  DaysInYearType,
31
+ Self,
31
32
  ValueType,
32
33
  )
33
34
 
@@ -49,7 +50,7 @@ def _random_generator(seed: int | None) -> Generator:
49
50
 
50
51
  """
51
52
  ss = SeedSequence(entropy=seed)
52
- bg = PCG64(seed=cast(int | None, ss))
53
+ bg = PCG64(seed=cast("int | None", ss))
53
54
  return Generator(bit_generator=bg)
54
55
 
55
56
 
@@ -121,7 +122,7 @@ class ReturnSimulation(BaseModel):
121
122
 
122
123
  """
123
124
  return cast(
124
- float,
125
+ "float",
125
126
  (
126
127
  self.results.ffill().pct_change().mean() * self.trading_days_in_year
127
128
  ).iloc[0],
@@ -138,7 +139,7 @@ class ReturnSimulation(BaseModel):
138
139
 
139
140
  """
140
141
  return cast(
141
- float,
142
+ "float",
142
143
  (
143
144
  self.results.ffill().pct_change().std()
144
145
  * sqrt(self.trading_days_in_year)
@@ -0,0 +1,27 @@
1
+ # BSD 3-Clause License
2
+
3
+ ## Copyright (c) Captor Fund Management AB
4
+
5
+ Redistribution and use in source and binary forms, with or without modification, are
6
+ permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this list of
9
+ conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list
12
+ of conditions and the following disclaimer in the documentation and/or other
13
+ materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its contributors may be
16
+ used to endorse or promote products derived from this software without specific prior
17
+ written permission.
18
+
19
+ This software is provided by the copyright holders and contributors “as is” and any
20
+ express or implied warranties, including, but not limited to, the implied warranties of
21
+ merchantability and fitness for a particular purpose, are disclaimed. In no event shall
22
+ the copyright holder or contributors be liable for any direct, indirect, incidental,
23
+ special, exemplary, or consequential damages (including, but not limited to, procurement
24
+ of substitute goods or services; loss of use, data, or profits; or business interruption)
25
+ however caused and on any theory of liability, whether in contract, strict liability,
26
+ or tort (including negligence or otherwise) arising in any way out of the use of this
27
+ software, even if advised of the possibility of such damage.